Files
OmniRoute/tests/unit/zed-hosted-think-close-marker.test.ts
Xiangzhe a9028e9571 fix(stream): suppress </think> close marker for Responses API clients (#7747)
* fix(stream): suppress `</think>` close marker for Responses API clients

The Claude→OpenAI `</think>` close marker (#4633) exists for Chat
Completions clients that scan content for the marker (Claude Code /
Cursor). On the openai-responses path the responsesTransformer already
maps reasoning_content to structured reasoning items, so the marker has
no consumer and leaks verbatim into response.output_text.delta —
observed in production with kimi-coding (k3): thinking renders
correctly while a stray `</think>` sits at the start of the assistant
text (up to 6 consecutive markers when the upstream also emits stray
close-tag text deltas).

resolveSuppressThinkClose() gains a clientResponseFormat option that
always suppresses the marker for openai-responses, winning over both
the UA allowlist and an explicit keep header (no legitimate marker
consumer exists in the Responses format). chatCore passes the format
through, and ExecuteInput now carries clientResponseFormat so the two
executors that do their own Claude→OpenAI translation apply the same
policy: GLM's Anthropic transport and zed-hosted's Anthropic backend
(which previously applied no suppression at all, not even the #5245
UA/header policy).

Chat Completions behavior is unchanged (#4633 / #5123 / #5245 / #5312).

* refactor(executors): extract helpers to keep execute/executeTransport under the complexity cap

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: xz-dev <xz-dev@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-19 14:46:37 -03:00

71 lines
2.7 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
const { __test__ } = await import("../../open-sse/executors/zed-hosted.ts");
const { wrapZedCompletionStream } = __test__;
// zed-hosted's Anthropic backend converts Claude events to OpenAI chunks
// inside the executor (wrapZedCompletionStream → claudeToOpenAIResponse),
// bypassing chatCore's suppressThinkClose wiring. Responses API clients
// receive reasoning as structured items, so the textual `</think>` close
// marker must be suppressed on that path (same policy as chatCore / GLM).
function buildZedAnthropicNdjson(): string {
const lines = [
{ event: { type: "message_start", message: { id: "msg_zed", model: "claude-test" } } },
{ event: { type: "content_block_start", index: 0, content_block: { type: "thinking" } } },
{
event: {
type: "content_block_delta",
index: 0,
delta: { type: "thinking_delta", thinking: "plan" },
},
},
{ event: { type: "content_block_stop", index: 0 } },
{ event: { type: "content_block_start", index: 1, content_block: { type: "text", text: "" } } },
{
event: { type: "content_block_delta", index: 1, delta: { type: "text_delta", text: "Hi" } },
},
{ event: { type: "content_block_stop", index: 1 } },
{
event: {
type: "message_delta",
delta: { stop_reason: "end_turn" },
usage: { output_tokens: 3 },
},
},
{ event: { type: "message_stop" } },
];
return lines.map((l) => JSON.stringify(l)).join("\n") + "\n";
}
async function readAll(stream: ReadableStream<Uint8Array>): Promise<string> {
const reader = stream.getReader();
const decoder = new TextDecoder();
let out = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
out += decoder.decode(value, { stream: true });
}
return out;
}
function wrapAnthropic(options?: Record<string, unknown>): Promise<string> {
const response = new Response(buildZedAnthropicNdjson(), { status: 200 });
const wrapped = wrapZedCompletionStream(response, "Anthropic", "claude-test", options);
return readAll(wrapped.body as ReadableStream<Uint8Array>);
}
test("zed anthropic stream keeps the close marker by default (#4633)", async () => {
const out = await wrapAnthropic();
assert.ok(out.includes('"content":"</think>"'), "expected default marker emission");
});
test("zed anthropic stream suppresses the close marker when asked", async () => {
const out = await wrapAnthropic({ suppressThinkClose: true });
assert.ok(!out.includes("</think>"), "marker must not leak into output");
assert.ok(out.includes('"content":"Hi"'), "text content still flows");
assert.ok(out.includes('"reasoning_content":"plan"'), "reasoning still flows");
});