diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json
index 755dca997e..ddf67c5fa2 100644
--- a/config/quality/file-size-baseline.json
+++ b/config/quality/file-size-baseline.json
@@ -215,7 +215,7 @@
"open-sse/translator/request/openai-to-gemini.ts": 906,
"open-sse/translator/request/openai-to-kiro.ts": 912,
"_rebaseline_2026_07_22_7936_namespace_roundtrip": "#7936 (@RCrushMe, Responses-Chat namespace round-trip identity seam) own growth: open-sse/translator/response/openai-responses.ts 1092->1125 (+33) and open-sse/utils/stream.ts 2814->2869 (+55) — threading the namespace-identity seam through the Responses↔Chat translation + stream paths so tool-call namespaces survive the round-trip. Cohesive translation/stream wiring at existing chokepoints, frozen at new size.",
- "open-sse/translator/response/openai-responses.ts": 1125,
+ "open-sse/translator/response/openai-responses.ts": 1137,
"open-sse/utils/cursorAgentProtobuf.ts": 1521,
"open-sse/utils/stream.ts": 2869,
"src/app/(dashboard)/dashboard/HomePageClient.tsx": 1385,
@@ -316,7 +316,8 @@
"_rebaseline_2026_07_12_v3847_mergeprs_tail": "v3.8.47 /merge-prs tail (owner-approved): src/lib/localDb.ts NEW>800 (799->805, +6 re-exports countFreeProxies + recordFreeProxySyncErrors/clearFreeProxySyncErrors/getFreeProxySyncErrors + FreeProxySyncErrors type for #6909 free-pool relay-repair; re-export-only per Hard Rule #2, not extractable).",
"_rebaseline_2026_07_21_8034_compression_exclusions_sidebar": "#8034 (compression exclusions dashboard tab) own growth: sections.ts 796->806 (+10, one new COMPRESSION_CONTEXT_GROUP sidebar item linking /dashboard/compression/exclusions). The file was already 796/800 before this PR (organic growth from prior sidebar entries), so a single new nav item pushed it 6 lines over cap. Freezing at 806 (cannot grow further); the sidebar item array is data, not extractable logic.",
"src/shared/constants/sidebarVisibility/sections.ts": 806,
- "_rebaseline_2026_07_22_8056_headroom_minrows": "#8056 (@RaviTharuma, persist Headroom minRows) own growth: src/lib/db/compression.ts 850->866 (+16 HeadroomConfig+DEFAULT_HEADROOM_CONFIG+normalize/store in get/updateCompressionSettings) and open-sse/services/compression/strategySelector.ts 1054->1060 (+6 merge settings.headroom into stacked stepConfig). Cohesive settings-persistence + stacked-merge wiring at existing chokepoints, frozen at new size."
+ "_rebaseline_2026_07_22_8056_headroom_minrows": "#8056 (@RaviTharuma, persist Headroom minRows) own growth: src/lib/db/compression.ts 850->866 (+16 HeadroomConfig+DEFAULT_HEADROOM_CONFIG+normalize/store in get/updateCompressionSettings) and open-sse/services/compression/strategySelector.ts 1054->1060 (+6 merge settings.headroom into stacked stepConfig). Cohesive settings-persistence + stacked-merge wiring at existing chokepoints, frozen at new size.",
+ "_rebaseline_2026_07_22_8081_reasoning_placeholder_guard": "#8081 (@Dingding-leo) own growth: openai-responses.ts 1125->1137 (+12) restructuring the reasoning-placeholder guard so it skips only the empty content block and still emits finish_reason/tool_calls in the same chunk. Cohesive translator wiring; frozen at new size."
},
"testCap": 800,
"testFrozen": {
diff --git a/open-sse/handlers/responseSanitizer.ts b/open-sse/handlers/responseSanitizer.ts
index 146e105c75..0e2ebd880c 100644
--- a/open-sse/handlers/responseSanitizer.ts
+++ b/open-sse/handlers/responseSanitizer.ts
@@ -2,6 +2,7 @@ import {
copyOpenAICompatibleReasoningFields,
getReadableReasoningValue,
} from "../utils/reasoningFields.ts";
+import { stripInternalReasoningPlaceholder } from "../utils/reasoningPlaceholder.ts";
import { normalizeOpenAICompatibleFinishReason } from "../utils/finishReason.ts";
import {
collapseExcessiveNewlines,
@@ -395,7 +396,9 @@ function sanitizeChoice(
function sanitizeMessageContent(msgRecord: JsonRecord, options: ParseOptions = {}): JsonRecord {
if (typeof msgRecord.content === "string") {
- const strippedContent = stripInternalToolEnvelopeText(msgRecord.content);
+ const strippedContent = stripInternalReasoningPlaceholder(
+ stripInternalToolEnvelopeText(msgRecord.content)
+ );
const nativeReasoning = getReadableReasoningValue(msgRecord);
const { content, thinking } =
options.parseTextualReasoningTags === true && !nativeReasoning
@@ -519,10 +522,7 @@ function sanitizeResponsesUsage(usage: unknown): unknown {
const inputDetails = toRecord(normalized.input_tokens_details) || {};
const cachedTokens = normalized.cached_tokens ?? normalized.cache_read_input_tokens;
- if (
- cachedTokens !== undefined &&
- inputDetails.cached_tokens === undefined
- ) {
+ if (cachedTokens !== undefined && inputDetails.cached_tokens === undefined) {
inputDetails.cached_tokens = cachedTokens;
}
if (
@@ -845,7 +845,9 @@ function sanitizeResponsesMessageContent(content: unknown): JsonRecord[] {
return [
{
type: "output_text",
- text: collapseExcessiveNewlines(stripInternalToolEnvelopeText(content)),
+ text: collapseExcessiveNewlines(
+ stripInternalReasoningPlaceholder(stripInternalToolEnvelopeText(content))
+ ),
annotations: [],
},
];
@@ -860,7 +862,9 @@ function sanitizeResponsesMessageContent(content: unknown): JsonRecord[] {
if (typeof part === "string") {
return {
type: "output_text",
- text: collapseExcessiveNewlines(stripInternalToolEnvelopeText(part)),
+ text: collapseExcessiveNewlines(
+ stripInternalReasoningPlaceholder(stripInternalToolEnvelopeText(part))
+ ),
annotations: [],
};
}
@@ -877,7 +881,9 @@ function sanitizeResponsesMessageContent(content: unknown): JsonRecord[] {
...partRecord,
type: "output_text",
text: collapseExcessiveNewlines(
- stripInternalToolEnvelopeText(toString(partRecord.text) || "")
+ stripInternalReasoningPlaceholder(
+ stripInternalToolEnvelopeText(toString(partRecord.text) || "")
+ )
),
annotations: Array.isArray(partRecord.annotations) ? partRecord.annotations : [],
};
diff --git a/open-sse/transformer/responsesTransformer.ts b/open-sse/transformer/responsesTransformer.ts
index ff9e86dfd8..3050930ac4 100644
--- a/open-sse/transformer/responsesTransformer.ts
+++ b/open-sse/transformer/responsesTransformer.ts
@@ -1,6 +1,9 @@
import { appendToolCallArgumentDelta } from "../utils/toolCallArguments.ts";
import { shouldParseTextualReasoningTags } from "../handlers/responseSanitizer.ts";
-import { isInternalReasoningPlaceholder } from "../utils/reasoningPlaceholder.ts";
+import {
+ isInternalReasoningPlaceholder,
+ stripInternalReasoningPlaceholder,
+} from "../utils/reasoningPlaceholder.ts";
import * as fs from "fs";
import * as path from "path";
/**
@@ -533,93 +536,104 @@ export function createResponsesApiTransformStream(
// Handle text content. Generic prompt-format tags are visible text;
// only tag-native models opt into textual reasoning extraction.
+ // Strip the internal reasoning placeholder if the model echoed it
+ // through ordinary content (#8081). Only the text-content emission
+ // is skipped when nothing meaningful remains — this must NOT skip
+ // this message's tool_calls / finish_reason handling below, so we
+ // gate the whole block on strippedContent instead of returning /
+ // continuing out of the msg loop early.
if (delta.content) {
- // Close reasoning if it was opened via native reasoning_content
- // and is still open, before emitting message content. Without this
- // the reasoning item is never closed and the message reuses the
- // reasoning output_index, producing a protocol-invalid stream.
- if (
- state.reasoningId &&
- !state.reasoningDone &&
- (!parseTextualReasoningTags || !state.inThinking)
- ) {
- closeReasoning(controller);
- }
-
- let content = delta.content;
-
- if (parseTextualReasoningTags) {
- if (content.includes("")) {
- state.inThinking = true;
- content = content.replaceAll("", "");
- startReasoning(controller, idx);
- }
-
- if (content.includes("")) {
- const parts = content.split("");
- const thinkPart = parts[0];
- const textPart = parts.slice(1).join("");
-
- if (thinkPart) emitReasoningDelta(controller, thinkPart);
+ const strippedContent = stripInternalReasoningPlaceholder(delta.content);
+ if (strippedContent) {
+ // Close reasoning if it was opened via native reasoning_content
+ // and is still open, before emitting message content. Without this
+ // the reasoning item is never closed and the message reuses the
+ // reasoning output_index, producing a protocol-invalid stream.
+ if (
+ state.reasoningId &&
+ !state.reasoningDone &&
+ (!parseTextualReasoningTags || !state.inThinking)
+ ) {
closeReasoning(controller);
- state.inThinking = false;
- content = textPart;
}
- if (state.inThinking && content) {
- emitReasoningDelta(controller, content);
- continue;
- }
- }
+ let content = strippedContent;
- // Regular text content
- if (content) {
- // Use a distinct output_index for the message when reasoning was
- // emitted, so the message item does not collide with the
- // reasoning item's output_index.
- const msgIdx = state.reasoningId ? state.reasoningIndex + 1 : idx;
+ if (parseTextualReasoningTags) {
+ if (content.includes("")) {
+ state.inThinking = true;
+ content = content.replaceAll("", "");
+ startReasoning(controller, idx);
+ }
- // Fix for #1211: Strip leading double-newlines / blank spaces from the very first text chunk
- if (!state.msgTextBuf[msgIdx]) {
- content = content.trimStart();
+ if (content.includes("")) {
+ const parts = content.split("");
+ const thinkPart = parts[0];
+ const textPart = parts.slice(1).join("");
+
+ if (thinkPart) emitReasoningDelta(controller, thinkPart);
+ closeReasoning(controller);
+ state.inThinking = false;
+ content = textPart;
+ }
+
+ if (state.inThinking && content) {
+ emitReasoningDelta(controller, content);
+ // Pre-existing behaviour (unrelated to #8081): a still-open
+ // textual block ends this message's handling early.
+ continue;
+ }
}
- if (!content) continue;
+ // Regular text content
+ if (content) {
+ // Use a distinct output_index for the message when reasoning was
+ // emitted, so the message item does not collide with the
+ // reasoning item's output_index.
+ const msgIdx = state.reasoningId ? state.reasoningIndex + 1 : idx;
- if (!state.msgItemAdded[msgIdx]) {
- state.msgItemAdded[msgIdx] = true;
- const msgId = `msg_${state.responseId}_${msgIdx}`;
+ // Fix for #1211: Strip leading double-newlines / blank spaces from the very first text chunk
+ if (!state.msgTextBuf[msgIdx]) {
+ content = content.trimStart();
+ }
- emit(controller, "response.output_item.added", {
- type: "response.output_item.added",
- output_index: msgIdx,
- item: { id: msgId, type: "message", content: [], role: "assistant" },
- });
- }
+ if (!content) continue;
- if (!state.msgContentAdded[msgIdx]) {
- state.msgContentAdded[msgIdx] = true;
+ if (!state.msgItemAdded[msgIdx]) {
+ state.msgItemAdded[msgIdx] = true;
+ const msgId = `msg_${state.responseId}_${msgIdx}`;
- emit(controller, "response.content_part.added", {
- type: "response.content_part.added",
+ emit(controller, "response.output_item.added", {
+ type: "response.output_item.added",
+ output_index: msgIdx,
+ item: { id: msgId, type: "message", content: [], role: "assistant" },
+ });
+ }
+
+ if (!state.msgContentAdded[msgIdx]) {
+ state.msgContentAdded[msgIdx] = true;
+
+ emit(controller, "response.content_part.added", {
+ type: "response.content_part.added",
+ item_id: `msg_${state.responseId}_${msgIdx}`,
+ output_index: msgIdx,
+ content_index: 0,
+ part: { type: "output_text", annotations: [], logprobs: [], text: "" },
+ });
+ }
+
+ emit(controller, "response.output_text.delta", {
+ type: "response.output_text.delta",
item_id: `msg_${state.responseId}_${msgIdx}`,
output_index: msgIdx,
content_index: 0,
- part: { type: "output_text", annotations: [], logprobs: [], text: "" },
+ delta: content,
+ logprobs: [],
});
+
+ if (!state.msgTextBuf[msgIdx]) state.msgTextBuf[msgIdx] = "";
+ state.msgTextBuf[msgIdx] += content;
}
-
- emit(controller, "response.output_text.delta", {
- type: "response.output_text.delta",
- item_id: `msg_${state.responseId}_${msgIdx}`,
- output_index: msgIdx,
- content_index: 0,
- delta: content,
- logprobs: [],
- });
-
- if (!state.msgTextBuf[msgIdx]) state.msgTextBuf[msgIdx] = "";
- state.msgTextBuf[msgIdx] += content;
}
}
diff --git a/open-sse/translator/response/openai-responses.ts b/open-sse/translator/response/openai-responses.ts
index f47cd1c5c0..2371db9adc 100644
--- a/open-sse/translator/response/openai-responses.ts
+++ b/open-sse/translator/response/openai-responses.ts
@@ -7,7 +7,10 @@ import { FORMATS } from "../formats.ts";
import { appendToolCallArgumentDelta } from "../../utils/toolCallArguments.ts";
import { fallbackToolCallId } from "../helpers/toolCallHelper.ts";
import { shouldParseTextualReasoningTags } from "../../handlers/responseSanitizer.ts";
-import { isInternalReasoningPlaceholder } from "../../utils/reasoningPlaceholder.ts";
+import {
+ isInternalReasoningPlaceholder,
+ stripInternalReasoningPlaceholder,
+} from "../../utils/reasoningPlaceholder.ts";
import {
normalizeToolName,
stripEmptyOptionalToolArgs,
@@ -168,43 +171,52 @@ export function openaiToOpenAIResponsesResponse(chunk, state) {
startReasoning(state, emit, idx);
emitReasoningDelta(state, emit, delta.reasoning_content);
}
+ // Strip the internal reasoning placeholder if the model echoed it
+ // through ordinary content (#8081). Only the text-content emission is
+ // skipped when nothing meaningful remains; tool_calls / finish_reason
+ // handling below must still run for this same chunk.
if (delta.content) {
- if (
- state.reasoningId &&
- !state.reasoningDone &&
- (!parseTextualReasoningTags || !state.inThinking)
- ) {
- closeReasoning(state, emit);
- }
-
- let content = delta.content;
-
- if (parseTextualReasoningTags) {
- if (content.includes("")) {
- state.inThinking = true;
- content = content.replaceAll("", "");
- startReasoning(state, emit, idx);
- }
-
- if (content.includes("")) {
- const parts = content.split("");
- const thinkPart = parts[0];
- const textPart = parts.slice(1).join("");
- if (thinkPart) emitReasoningDelta(state, emit, thinkPart);
+ const strippedContent = stripInternalReasoningPlaceholder(delta.content);
+ if (strippedContent) {
+ if (
+ state.reasoningId &&
+ !state.reasoningDone &&
+ (!parseTextualReasoningTags || !state.inThinking)
+ ) {
closeReasoning(state, emit);
- state.inThinking = false;
- content = textPart;
}
- if (state.inThinking && content) {
- emitReasoningDelta(state, emit, content);
- return events;
- }
- }
+ let content = strippedContent;
- if (content) {
- const msgIdx = state.reasoningId ? state.reasoningIndex + 1 : idx;
- emitTextContent(state, emit, msgIdx, content);
+ if (parseTextualReasoningTags) {
+ if (content.includes("")) {
+ state.inThinking = true;
+ content = content.replaceAll("", "");
+ startReasoning(state, emit, idx);
+ }
+
+ if (content.includes("")) {
+ const parts = content.split("");
+ const thinkPart = parts[0];
+ const textPart = parts.slice(1).join("");
+ if (thinkPart) emitReasoningDelta(state, emit, thinkPart);
+ closeReasoning(state, emit);
+ state.inThinking = false;
+ content = textPart;
+ }
+
+ if (state.inThinking && content) {
+ emitReasoningDelta(state, emit, content);
+ // Pre-existing behaviour (unrelated to #8081): a still-open
+ // textual block ends this chunk's handling early.
+ return events;
+ }
+ }
+
+ if (content) {
+ const msgIdx = state.reasoningId ? state.reasoningIndex + 1 : idx;
+ emitTextContent(state, emit, msgIdx, content);
+ }
}
}
diff --git a/open-sse/translator/response/openai-to-claude.ts b/open-sse/translator/response/openai-to-claude.ts
index de34ddc139..ffe1887068 100644
--- a/open-sse/translator/response/openai-to-claude.ts
+++ b/open-sse/translator/response/openai-to-claude.ts
@@ -4,7 +4,10 @@ import { CLAUDE_OAUTH_TOOL_PREFIX } from "../request/openai-to-claude.ts";
import { hasToolCallShim, applyToolCallShimToBuffer } from "../helpers/toolCallShim.ts";
import { appendToolCallArgumentDelta } from "../../utils/toolCallArguments.ts";
import { isAbortFinishReason } from "../../utils/finishReason.ts";
-import { isInternalReasoningPlaceholder } from "../../utils/reasoningPlaceholder.ts";
+import {
+ isInternalReasoningPlaceholder,
+ stripInternalReasoningPlaceholder,
+} from "../../utils/reasoningPlaceholder.ts";
import { REVERSE_MAP } from "../../services/claudeCodeToolRemapper.ts";
function normalizeToolName(name: string): string {
@@ -208,58 +211,64 @@ export function openaiToClaudeResponse(chunk, state) {
});
}
- // Handle regular content
+ // Handle regular content — strip the internal reasoning placeholder if
+ // the model echoed it through ordinary content (#8081). Only the content
+ // block emission is skipped when nothing meaningful remains; the chunk
+ // may still carry tool_calls / finish_reason below, which must still run.
if (delta?.content) {
- stopThinkingBlock(state, results);
+ const strippedContent = stripInternalReasoningPlaceholder(delta.content);
+ if (strippedContent) {
+ stopThinkingBlock(state, results);
- // Check for XML blocks that some models emit instead of JSON tool_calls
- const { cleaned, toolCalls: xmlToolCalls } = extractXmlInvokeBlocks(delta.content, state);
+ // Check for XML blocks that some models emit instead of JSON tool_calls
+ const { cleaned, toolCalls: xmlToolCalls } = extractXmlInvokeBlocks(strippedContent, state);
- // Accumulate extracted tool calls for emission at finish
- if (xmlToolCalls.length > 0) {
- // Close any ongoing text block before tool calls
- stopTextBlock(state, results);
- state._pendingXmlToolCalls.push(...xmlToolCalls);
- }
+ // Accumulate extracted tool calls for emission at finish
+ if (xmlToolCalls.length > 0) {
+ // Close any ongoing text block before tool calls
+ stopTextBlock(state, results);
+ state._pendingXmlToolCalls.push(...xmlToolCalls);
+ }
- // Emit remaining non-XML text content
- if (!cleaned) {
- // All content was XML invoke blocks — skip text block entirely
- // (tool calls will be emitted at finish)
- } else if (xmlToolCalls.length > 0) {
- // Text before/between/after XML blocks — (re)start a text block
- if (!state.textBlockStarted) {
- state.textBlockIndex = state.nextBlockIndex++;
- state.textBlockStarted = true;
- state.textBlockClosed = false;
+ // Emit remaining non-XML text content
+ if (!cleaned) {
+ // All content was XML invoke blocks — skip text block entirely
+ // (tool calls will be emitted at finish)
+ } else if (xmlToolCalls.length > 0) {
+ // Text before/between/after XML blocks — (re)start a text block
+ if (!state.textBlockStarted) {
+ state.textBlockIndex = state.nextBlockIndex++;
+ state.textBlockStarted = true;
+ state.textBlockClosed = false;
+ results.push({
+ type: "content_block_start",
+ index: state.textBlockIndex,
+ content_block: { type: "text", text: "" },
+ });
+ }
results.push({
- type: "content_block_start",
+ type: "content_block_delta",
index: state.textBlockIndex,
- content_block: { type: "text", text: "" },
+ delta: { type: "text_delta", text: cleaned },
+ });
+ } else {
+ // No XML — emit as regular text (original behaviour)
+ if (!state.textBlockStarted) {
+ state.textBlockIndex = state.nextBlockIndex++;
+ state.textBlockStarted = true;
+ state.textBlockClosed = false;
+ results.push({
+ type: "content_block_start",
+ index: state.textBlockIndex,
+ content_block: { type: "text", text: "" },
+ });
+ }
+ results.push({
+ type: "content_block_delta",
+ index: state.textBlockIndex,
+ delta: { type: "text_delta", text: cleaned },
});
}
- results.push({
- type: "content_block_delta",
- index: state.textBlockIndex,
- delta: { type: "text_delta", text: cleaned },
- });
- } else {
- // No XML — emit as regular text (original behaviour)
- if (!state.textBlockStarted) {
- state.textBlockIndex = state.nextBlockIndex++;
- state.textBlockStarted = true;
- state.textBlockClosed = false;
- results.push({
- type: "content_block_start",
- index: state.textBlockIndex,
- content_block: { type: "text", text: "" },
- });
- }
- results.push({
- type: "content_block_delta",
- index: state.textBlockIndex,
- delta: { type: "text_delta", text: cleaned },
- });
}
}
@@ -380,7 +389,12 @@ export function openaiToClaudeResponse(chunk, state) {
results.push({
type: "content_block_start",
index: toolInfo.blockIndex,
- content_block: { type: "tool_use", id: toolInfo.id, name: toolInfo.name || "", input: {} },
+ content_block: {
+ type: "tool_use",
+ id: toolInfo.id,
+ name: toolInfo.name || "",
+ input: {},
+ },
});
}
diff --git a/open-sse/utils/reasoningPlaceholder.ts b/open-sse/utils/reasoningPlaceholder.ts
index d3669ba2b6..1531c9cb00 100644
--- a/open-sse/utils/reasoningPlaceholder.ts
+++ b/open-sse/utils/reasoningPlaceholder.ts
@@ -8,3 +8,13 @@ export const NON_ANTHROPIC_THINKING_PLACEHOLDER = "(prior reasoning summary unav
export function isInternalReasoningPlaceholder(value: unknown): boolean {
return typeof value === "string" && value.trim() === NON_ANTHROPIC_THINKING_PLACEHOLDER;
}
+
+/**
+ * Strip the internal placeholder from user-visible content. Models sometimes
+ * echo the sentinel through ordinary `message.content` / `delta.content`
+ * (#8081). Removes all occurrences and trims; returns "" when nothing
+ * meaningful remains so callers can skip emission entirely.
+ */
+export function stripInternalReasoningPlaceholder(value: string): string {
+ return value.replaceAll(NON_ANTHROPIC_THINKING_PLACEHOLDER, "").trim();
+}
diff --git a/tests/unit/translator-resp-openai-to-claude.test.ts b/tests/unit/translator-resp-openai-to-claude.test.ts
index 1d831a05f5..f204c7d137 100644
--- a/tests/unit/translator-resp-openai-to-claude.test.ts
+++ b/tests/unit/translator-resp-openai-to-claude.test.ts
@@ -113,6 +113,59 @@ test("OpenAI stream: internal reasoning replay placeholder stays hidden from Cla
assert.equal(result[2].delta.text, "Answer");
});
+test("OpenAI stream: placeholder-only content bundled with finish_reason still emits the stop event (#8081 regression)", () => {
+ const state = createState();
+ // Start a real message with some text first.
+ const first = openaiToClaudeResponse(
+ {
+ id: "chatcmpl-2c",
+ model: "gpt-4.1",
+ choices: [{ index: 0, delta: { content: "Answer" }, finish_reason: null }],
+ },
+ state
+ );
+ // Final chunk carries the internal reasoning placeholder as ordinary content
+ // AND the finish_reason in the SAME chunk. The placeholder content must be
+ // suppressed, but the stop event must still fire (the pre-fix bare `return`
+ // dropped the finish entirely — this is the regression guard).
+ const final = openaiToClaudeResponse(
+ {
+ id: "chatcmpl-2c",
+ model: "gpt-4.1",
+ choices: [
+ {
+ index: 0,
+ delta: { content: "(prior reasoning summary unavailable)" },
+ finish_reason: "stop",
+ },
+ ],
+ usage: { prompt_tokens: 4, completion_tokens: 1, total_tokens: 5 },
+ },
+ state
+ );
+ const result = flatten([first, final]);
+
+ // The placeholder text is never emitted as a visible text delta.
+ assert.equal(
+ result.some(
+ (event) =>
+ event.type === "content_block_delta" &&
+ event.delta?.type === "text_delta" &&
+ typeof event.delta.text === "string" &&
+ event.delta.text.includes("prior reasoning summary unavailable")
+ ),
+ false
+ );
+ // The stop event still fires despite the placeholder-only final content.
+ const stop = result.find((event) => event.type === "message_delta");
+ assert.ok(stop, "expected a message_delta stop event even with placeholder-only final content");
+ assert.equal(stop.delta.stop_reason, "end_turn");
+ assert.ok(
+ result.some((event) => event.type === "message_stop"),
+ "expected a message_stop event"
+ );
+});
+
test("OpenAI stream: tool calls strip Claude OAuth prefix and keep cache usage", () => {
const state = createState();
const started = openaiToClaudeResponse(