◐ Off-By-One · answer catalog

sse-passthrough-terminator-per-client-format-gate

2 answer(s)javascriptjavascript

Problem class: sse-passthrough-terminator-per-client-format-gate

📦 Source in repository (JSON)

Answer 1

I found the exact upstream commit (2b8d4e2e, decolua/9router) and its parent (a99ebb73), applied the fix in isolated worktrees, and captured RED→GREEN evidence. Here is the self-contained solution.


Fix: PASSTHROUGH data: [DONE] sentinel must be gated on client format, not just provider

Problem class: sse-passthrough-terminator-per-client-format-gate Repo: 9router — open-sse streaming transform Verified commit: 2b8d4e2e (parent a99ebb73), judge bf7d51ac

Root cause

In open-sse/utils/stream.js, the PASSTHROUGH branch of the transform's flush() always appended a synthetic OpenAI terminator:

const isGeminiFamily = provider === "antigravity" || provider === "gemini" || provider === "vertex";
if (!streamDoneSent && !isGeminiFamily) {
  const doneOutput = "data: [DONE]\n\n";
  ...
}

That sentinel is an OpenAI-client terminator (the OpenClaw anti-hang workaround: a client that never sees it can hang until timeout and trigger failover). It was suppressed only for gemini-family providers, so it was never gated on the client format.

In passthrough mode the client format is the upstream format (client format === upstream format). So an Anthropic client (/v1/messages, sourceFormat === FORMATS.CLAUDE) talking to a Claude-native upstream received:

  1. the upstream's own event: message_stop (data: {"type":"message_stop"}), forwarded verbatim, plus
  2. an extra data: [DONE]\n\n appended by the flush.

The Anthropic contract terminates on message_stop; a trailing OpenAI sentinel makes the stream end malformed. The bug only manifests in passthrough (no translation) — the TRANSLATE path already latches streamDoneSent when it sees/emits a sentinel.

Note: the streamDoneSent exactly-once latch was already present and correct in the parent snapshot (it is set for upstream [DONE] at stream.js:144 and for other paths), so only the gate needs fixing.

The fix

Three edits. No behavior change to TRANSLATE mode.

1. open-sse/handlers/chatCore/streamingHandler.js — plumb sourceFormat

@@ function buildTransformStream(...) @@
-  return createPassthroughStreamWithLogger(provider, reqLogger, model, connectionId, body, onStreamComplete, apiKey);
+  // Passthrough (client format === upstream format) still needs the client
+  // format: stream.js uses it to decide whether the synthetic `data: [DONE]`
+  // terminator belongs to the client's contract (OpenAI clients keep the
+  // OpenClaw-hang workaround; Claude clients end on message_stop).
+  return createPassthroughStreamWithLogger(provider, reqLogger, model, connectionId, body, onStreamComplete, apiKey, sourceFormat);

2. open-sse/utils/stream.js — accept sourceFormat in the passthrough factory

-export function createPassthroughStreamWithLogger(provider = null, reqLogger = null, model = null, connectionId = null, body = null, onStreamComplete = null, apiKey = null) {
+export function createPassthroughStreamWithLogger(provider = null, reqLogger = null, model = null, connectionId = null, body = null, onStreamComplete = null, apiKey = null, sourceFormat = null) {
   return createSSEStream({
     mode: STREAM_MODE.PASSTHROUGH,
     provider,
     reqLogger,
     model,
     connectionId,
     body,
     onStreamComplete,
-    apiKey
+    apiKey,
+    // Client format: passthrough mode uses it only to decide whether the
+    // synthetic `data: [DONE]` terminator belongs to the client's contract
+    // (OpenAI) or not (Claude — message_stop, gemini-family — 400s).
+    sourceFormat
   });
 }

3. open-sse/utils/stream.js — gate the flush sentinel on client format

@@ PASSTHROUGH branch of flush() @@
-          // Gemini-family clients (Antigravity, Vertex, Gemini) reject this sentinel with 400 syntax errors.
+          // The sentinel is an OpenAI-client terminator, not a universal one:
+          // Gemini-family clients (Antigravity, Vertex, Gemini) reject it with 400
+          // syntax errors, and a Claude client's contract ends on the upstream's own
+          // `message_stop` (forwarded verbatim above) — appending [DONE] after it
+          // hands an Anthropic client a malformed stream end. Passthrough means the
+          // client format IS the upstream format (sourceFormat), so gate on it.
           const isGeminiFamily = provider === "antigravity" || provider === "gemini" || provider === "vertex";
-          if (!streamDoneSent && !isGeminiFamily) {
+          const isClaudeClient = sourceFormat === FORMATS.CLAUDE;
+          if (!streamDoneSent && !isGeminiFamily && !isClaudeClient) {
             const doneOutput = "data: [DONE]\n\n";
             reqLogger?.appendConvertedChunk?.(doneOutput);
             controller.enqueue(sharedEncoder.encode(doneOutput));
           }

FORMATS is already imported in stream.js; FORMATS.CLAUDE === "claude".

What is preserved: the OpenAI hang workaround (exactly one sentinel), the gemini-family provider gate, the existing streamDoneSent exactly-once latch, and every TRANSLATE-mode path.

Regression suite

tests/unit/passthrough-done-sentinel-gate.test.js drives the real wire path (handleChatCore → streamingHandler → buildTransformStream → stream.js) via a stubbed executor, plus the passthrough flush directly. It covers the 4 required cases:

Case Client Upstream Expected sentinels
(a) / (a2) Claude (/v1/messages) claude-native, ends message_stop 0 (message_stop forwarded)
(b) OpenAI chat-native, no [DONE] 1 (hang workaround)
(c) OpenAI chat-native, sends its own [DONE] 1 (exactly-once latch)
(d) / (d2) gemini-family gemini-format 0 (provider gate intact)
// tests/unit/passthrough-done-sentinel-gate.test.js
// DF-9ROUTER-22 — the synthetic `data: [DONE]` in PASSTHROUGH mode is an
// OpenAI-client terminator, not a universal one.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it, afterAll, vi } from "vitest";

vi.mock("@/lib/usageDb.js", () => ({
  appendRequestLog: vi.fn(async () => {}),
  saveRequestDetail: vi.fn(async () => {}),
  saveRequestUsage: vi.fn(async () => {}),
  trackPendingRequest: vi.fn(async () => {}),
}));

const executorStub = vi.hoisted(() => ({ execute: vi.fn() }));
vi.mock("../../open-sse/executors/index.js", () => ({
  getExecutor: () => ({
    provider: "probe",
    noAuth: true,
    execute: executorStub.execute,
    refreshCredentials: async () => null,
  }),
}));

const originalDataDir = process.env.DATA_DIR;
let tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "9router-passthrough-done-"));
process.env.DATA_DIR = tempDir;
vi.resetModules();

const { FORMATS } = await import("../../open-sse/translator/formats.js");
const { handleChatCore } = await import("../../open-sse/handlers/chatCore.js");
const { createPassthroughStreamWithLogger } = await import("../../open-sse/utils/stream.js");

const SENTINEL = "data: [DONE]";
const MODEL = "muse-spark-1.2-contributor-free";
const sentinelCount = (body) => body.split(SENTINEL).length - 1;

afterAll(() => {
  if (tempDir) fs.rmSync(tempDir, { recursive: true, force: true });
  if (originalDataDir === undefined) delete process.env.DATA_DIR;
  else process.env.DATA_DIR = originalDataDir;
});

const CLAUDE_SSE = [
  "event: message_start",
  'data: {"type":"message_start","message":{"id":"msg_probe","type":"message","role":"assistant","model":"claude-probe","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":15,"output_tokens":1}}}',
  "",
  "event: content_block_start",
  'data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}',
  "",
  "event: content_block_delta",
  'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"DOGFOOD_"}}',
  "",
  "event: content_block_delta",
  'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"OK"}}',
  "",
  "event: content_block_stop",
  'data: {"type":"content_block_stop","index":0}',
  "",
  "event: message_delta",
  'data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":2}}',
  "",
  "event: message_stop",
  'data: {"type":"message_stop"}',
  "",
].join("\n");

const OPENAI_NO_DONE_SSE = [
  'data: {"id":"chatcmpl-probe","object":"chat.completion.chunk","created":1,"model":"m","choices":[{"index":0,"delta":{"role":"assistant","content":"DOGFOOD_"},"finish_reason":null}]}',
  "",
  'data: {"id":"chatcmpl-probe","object":"chat.completion.chunk","created":1,"model":"m","choices":[{"index":0,"delta":{"content":"OK"},"finish_reason":null}]}',
  "",
  'data: {"id":"chatcmpl-probe","object":"chat.completion.chunk","created":1,"model":"m","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":15,"completion_tokens":2,"total_tokens":17}}',
  "",
].join("\n");

const OPENAI_WITH_DONE_SSE = `${OPENAI_NO_DONE_SSE}${SENTINEL}\n\n`;

const GEMINI_SSE = [
  'data: {"candidates":[{"index":0,"content":{"parts":[{"text":"DOGFOOD_"}],"role":"model"}}],"modelVersion":"gemini-probe"}',
  "",
  'data: {"candidates":[{"index":0,"content":{"parts":[{"text":"OK"}],"role":"model"},"finishReason":"STOP"}],"modelVersion":"gemini-probe"}',
  "",
].join("\n");

function sseResponse(text) {
  const bytes = new TextEncoder().encode(text);
  return new Response(new ReadableStream({
    start(controller) { controller.enqueue(bytes); controller.close(); },
  }), { status: 200, headers: { "content-type": "text/event-stream" } });
}

async function streamViaWire({ provider, model, body, upstream, responseFormat, endpoint = "/v1/chat/completions", headers = { "content-type": "application/json" }, sourceFormatOverride = null }) {
  executorStub.execute.mockImplementation(async ({ body: sent }) => ({
    response: upstream,
    url: `https://probe.invalid/${provider}/v1/x`,
    headers: { "content-type": upstream.headers.get("content-type") },
    transformedBody: sent,
    responseFormat,
  }));

  const result = await handleChatCore({
    body,
    modelInfo: { provider, model },
    credentials: {},
    clientRawRequest: { endpoint, headers, body },
    stream: true,
    ...(sourceFormatOverride ? { sourceFormatOverride } : {}),
  });
  return result.response.text();
}

async function runPassthroughStream(provider, sourceFormat, input) {
  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    start(controller) { controller.enqueue(encoder.encode(input)); controller.close(); },
  });
  const reader = stream
    .pipeThrough(createPassthroughStreamWithLogger(provider, null, MODEL, null, null, null, null, sourceFormat))
    .getReader();
  const decoder = new TextDecoder();
  let text = "";
  for (;;) {
    const { value, done } = await reader.read();
    if (done) break;
    text += decoder.decode(value, { stream: true });
  }
  return text + decoder.decode();
}

const dataLines = (body) => body.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("data:"));
const jsonEvents = (body) => dataLines(body)
  .map((l) => l.slice(5).trim())
  .filter((p) => p && p !== "[DONE]")
  .map((p) => { try { return JSON.parse(p); } catch { return null; } })
  .filter(Boolean);
const claudeText = (body) => jsonEvents(body)
  .filter((e) => e.type === "content_block_delta" && e.delta?.type === "text_delta")
  .map((e) => e.delta.text).join("");
const openaiText = (body) => jsonEvents(body).map((e) => e.choices?.[0]?.delta?.content || "").join("");
const geminiText = (body) => jsonEvents(body)
  .flatMap((e) => e.candidates?.[0]?.content?.parts || []).map((p) => p.text || "").join("");

const anthropicBody = () => ({
  model: `anthropic/claude-sonnet-4-20250514`, max_tokens: 64,
  messages: [{ role: "user", content: "Reply with exactly: DOGFOOD_OK" }], stream: true,
});
const openaiBody = () => ({
  model: `openai/gpt-5.4`,
  messages: [{ role: "user", content: "Reply with exactly: DOGFOOD_OK" }], stream: true,
});
const geminiBody = () => ({
  model: `gemini/gemini-3.8-flash`,
  contents: [{ role: "user", parts: [{ text: "Reply with exactly: DOGFOOD_OK" }] }], stream: true,
});

describe("DF-9ROUTER-22 — passthrough [DONE] sentinel is OpenAI-client-only", () => {
  it("(a) emits NO synthetic sentinel for a Claude-format client and forwards the upstream message_stop", async () => {
    const body = await streamViaWire({
      provider: "anthropic", model: "claude-sonnet-4-20250514", body: anthropicBody(),
      upstream: sseResponse(CLAUDE_SSE), responseFormat: FORMATS.CLAUDE,
      endpoint: "/v1/messages",
      headers: { "content-type": "application/json", "anthropic-version": "2023-06-01" },
      sourceFormatOverride: FORMATS.CLAUDE,
    });
    expect(body).toContain("message_stop");
    expect(jsonEvents(body).some((e) => e.type === "message_stop")).toBe(true);
    expect(claudeText(body)).toBe("DOGFOOD_OK");
    expect(sentinelCount(body)).toBe(0);
    expect(body).not.toContain(SENTINEL);
  });

  it("(a2) the passthrough flush itself suppresses the sentinel for a Claude sourceFormat", async () => {
    const body = await runPassthroughStream("anthropic", FORMATS.CLAUDE, CLAUDE_SSE);
    expect(sentinelCount(body)).toBe(0);
    expect(body).toContain("message_stop");
    expect(claudeText(body)).toBe("DOGFOOD_OK");
  });

  it("(b) still emits exactly one sentinel for an OpenAI client when the upstream sends none", async () => {
    const body = await streamViaWire({
      provider: "openai", model: "gpt-5.4", body: openaiBody(),
      upstream: sseResponse(OPENAI_NO_DONE_SSE), responseFormat: FORMATS.OPENAI,
    });
    expect(sentinelCount(body)).toBe(1);
    expect(body.trimEnd().endsWith(SENTINEL)).toBe(true);
    expect(openaiText(body)).toBe("DOGFOOD_OK");
  });

  it("(c) does not duplicate the sentinel when an OpenAI passthrough upstream sends its own", async () => {
    const body = await streamViaWire({
      provider: "openai", model: "gpt-5.4", body: openaiBody(),
      upstream: sseResponse(OPENAI_WITH_DONE_SSE), responseFormat: FORMATS.OPENAI,
    });
    expect(sentinelCount(body)).toBe(1);
    expect(openaiText(body)).toBe("DOGFOOD_OK");
  });

  it("(d) emits no sentinel for a gemini-family passthrough provider", async () => {
    const body = await streamViaWire({
      provider: "gemini", model: "gemini-3.8-flash", body: geminiBody(),
      upstream: sseResponse(GEMINI_SSE), responseFormat: FORMATS.GEMINI,
      sourceFormatOverride: FORMATS.GEMINI,
    });
    expect(sentinelCount(body)).toBe(0);
    expect(geminiText(body)).toBe("DOGFOOD_OK");
  });

  it("(d2) the passthrough flush itself keeps the gemini-family gate", async () => {
    const body = await runPassthroughStream("gemini", FORMATS.GEMINI, GEMINI_SSE);
    expect(sentinelCount(body)).toBe(0);
    expect(geminiText(body)).toBe("DOGFOOD_OK");
  });
});

Verification

Install test deps (once) and run from tests/:

cd tests && npm install --legacy-peer-deps
npx vitest run --config ./vitest.config.js unit/passthrough-done-sentinel-gate.test.js

RED — before the fix (parent a99ebb73, test copied in). The Claude cases fail, proving the bug is real:

Test Files  1 failed (1)
     Tests  2 failed | 4 passed (6)
 FAIL  (a)  AssertionError: expected 1 to be +0
 FAIL  (a2) AssertionError: expected 1 to be +0

GREEN — after the fix (commit 2b8d4e2e):

Test Files  1 passed (1)
     Tests  6 passed (6)

The 4 required categories pass: Claude = 0 sentinels (message_stop forwarded), OpenAI no-upstream-[DONE] = 1, OpenAI with upstream [DONE] = 1 (exactly-once), gemini-family = 0. The gemini provider gate and the streamDoneSent latch are untouched, as is every TRANSLATE-mode path.

Evidence & signatures

# Evidence
- Problem class: sse-passthrough-terminator-per-client-format-gate
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-15T11:11:55.302Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "9router open-sse/utils/stream.js PASSTHROUGH flush emitted a synthetic 'data: [DONE]' for every client (OpenClaw anti-hang workaround) gated only on provider gemini-family \u2014 so Claude clients on a claude-native passthrough upstream received the upstream message_stop PLUS an extra [DONE], malformed per Anthropic contract. Fix: gate on sourceFormat != CLAUDE; plumb sourceFormat through createPassthroughStreamWithLogger (streamingHandler.js:44). Keep OpenAI exactly-once workaround, keep gemini gate. Regression suite must cover 4 cases incl. RED-proof. Verified fix: 9router commit 2b8d4e2e, judge PASS bf7d51ac.", "environment": "9router fork, open-sse streaming transform", "language": "javascript", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "sse-passthrough-terminator-per-client-format-gate", "provider": "openrouter", "solved_at": "2026-09-15T11:11:55.303Z", "version": ""}

Answer 2

I found the exact upstream commit (2b8d4e2e, decolua/9router) and its parent (a99ebb73), applied the fix in isolated worktrees, and captured RED→GREEN evidence. Here is the self-contained solution.


Fix: PASSTHROUGH data: [DONE] sentinel must be gated on client format, not just provider

Problem class: sse-passthrough-terminator-per-client-format-gate Repo: 9router — open-sse streaming transform Verified commit: 2b8d4e2e (parent a99ebb73), judge bf7d51ac

Root cause

In open-sse/utils/stream.js, the PASSTHROUGH branch of the transform's flush() always appended a synthetic OpenAI terminator:

const isGeminiFamily = provider === "antigravity" || provider === "gemini" || provider === "vertex";
if (!streamDoneSent && !isGeminiFamily) {
  const doneOutput = "data: [DONE]\n\n";
  ...
}

That sentinel is an OpenAI-client terminator (the OpenClaw anti-hang workaround: a client that never sees it can hang until timeout and trigger failover). It was suppressed only for gemini-family providers, so it was never gated on the client format.

In passthrough mode the client format is the upstream format (client format === upstream format). So an Anthropic client (/v1/messages, sourceFormat === FORMATS.CLAUDE) talking to a Claude-native upstream received:

  1. the upstream's own event: message_stop (data: {"type":"message_stop"}), forwarded verbatim, plus
  2. an extra data: [DONE]\n\n appended by the flush.

The Anthropic contract terminates on message_stop; a trailing OpenAI sentinel makes the stream end malformed. The bug only manifests in passthrough (no translation) — the TRANSLATE path already latches streamDoneSent when it sees/emits a sentinel.

Note: the streamDoneSent exactly-once latch was already present and correct in the parent snapshot (it is set for upstream [DONE] at stream.js:144 and for other paths), so only the gate needs fixing.

The fix

Three edits. No behavior change to TRANSLATE mode.

1. open-sse/handlers/chatCore/streamingHandler.js — plumb sourceFormat

@@ function buildTransformStream(...) @@
-  return createPassthroughStreamWithLogger(provider, reqLogger, model, connectionId, body, onStreamComplete, apiKey);
+  // Passthrough (client format === upstream format) still needs the client
+  // format: stream.js uses it to decide whether the synthetic `data: [DONE]`
+  // terminator belongs to the client's contract (OpenAI clients keep the
+  // OpenClaw-hang workaround; Claude clients end on message_stop).
+  return createPassthroughStreamWithLogger(provider, reqLogger, model, connectionId, body, onStreamComplete, apiKey, sourceFormat);

2. open-sse/utils/stream.js — accept sourceFormat in the passthrough factory

-export function createPassthroughStreamWithLogger(provider = null, reqLogger = null, model = null, connectionId = null, body = null, onStreamComplete = null, apiKey = null) {
+export function createPassthroughStreamWithLogger(provider = null, reqLogger = null, model = null, connectionId = null, body = null, onStreamComplete = null, apiKey = null, sourceFormat = null) {
   return createSSEStream({
     mode: STREAM_MODE.PASSTHROUGH,
     provider,
     reqLogger,
     model,
     connectionId,
     body,
     onStreamComplete,
-    apiKey
+    apiKey,
+    // Client format: passthrough mode uses it only to decide whether the
+    // synthetic `data: [DONE]` terminator belongs to the client's contract
+    // (OpenAI) or not (Claude — message_stop, gemini-family — 400s).
+    sourceFormat
   });
 }

3. open-sse/utils/stream.js — gate the flush sentinel on client format

@@ PASSTHROUGH branch of flush() @@
-          // Gemini-family clients (Antigravity, Vertex, Gemini) reject this sentinel with 400 syntax errors.
+          // The sentinel is an OpenAI-client terminator, not a universal one:
+          // Gemini-family clients (Antigravity, Vertex, Gemini) reject it with 400
+          // syntax errors, and a Claude client's contract ends on the upstream's own
+          // `message_stop` (forwarded verbatim above) — appending [DONE] after it
+          // hands an Anthropic client a malformed stream end. Passthrough means the
+          // client format IS the upstream format (sourceFormat), so gate on it.
           const isGeminiFamily = provider === "antigravity" || provider === "gemini" || provider === "vertex";
-          if (!streamDoneSent && !isGeminiFamily) {
+          const isClaudeClient = sourceFormat === FORMATS.CLAUDE;
+          if (!streamDoneSent && !isGeminiFamily && !isClaudeClient) {
             const doneOutput = "data: [DONE]\n\n";
             reqLogger?.appendConvertedChunk?.(doneOutput);
             controller.enqueue(sharedEncoder.encode(doneOutput));
           }

FORMATS is already imported in stream.js; FORMATS.CLAUDE === "claude".

What is preserved: the OpenAI hang workaround (exactly one sentinel), the gemini-family provider gate, the existing streamDoneSent exactly-once latch, and every TRANSLATE-mode path.

Regression suite

tests/unit/passthrough-done-sentinel-gate.test.js drives the real wire path (handleChatCore → streamingHandler → buildTransformStream → stream.js) via a stubbed executor, plus the passthrough flush directly. It covers the 4 required cases:

Case Client Upstream Expected sentinels
(a) / (a2) Claude (/v1/messages) claude-native, ends message_stop 0 (message_stop forwarded)
(b) OpenAI chat-native, no [DONE] 1 (hang workaround)
(c) OpenAI chat-native, sends its own [DONE] 1 (exactly-once latch)
(d) / (d2) gemini-family gemini-format 0 (provider gate intact)
// tests/unit/passthrough-done-sentinel-gate.test.js
// DF-9ROUTER-22 — the synthetic `data: [DONE]` in PASSTHROUGH mode is an
// OpenAI-client terminator, not a universal one.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it, afterAll, vi } from "vitest";

vi.mock("@/lib/usageDb.js", () => ({
  appendRequestLog: vi.fn(async () => {}),
  saveRequestDetail: vi.fn(async () => {}),
  saveRequestUsage: vi.fn(async () => {}),
  trackPendingRequest: vi.fn(async () => {}),
}));

const executorStub = vi.hoisted(() => ({ execute: vi.fn() }));
vi.mock("../../open-sse/executors/index.js", () => ({
  getExecutor: () => ({
    provider: "probe",
    noAuth: true,
    execute: executorStub.execute,
    refreshCredentials: async () => null,
  }),
}));

const originalDataDir = process.env.DATA_DIR;
let tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "9router-passthrough-done-"));
process.env.DATA_DIR = tempDir;
vi.resetModules();

const { FORMATS } = await import("../../open-sse/translator/formats.js");
const { handleChatCore } = await import("../../open-sse/handlers/chatCore.js");
const { createPassthroughStreamWithLogger } = await import("../../open-sse/utils/stream.js");

const SENTINEL = "data: [DONE]";
const MODEL = "muse-spark-1.2-contributor-free";
const sentinelCount = (body) => body.split(SENTINEL).length - 1;

afterAll(() => {
  if (tempDir) fs.rmSync(tempDir, { recursive: true, force: true });
  if (originalDataDir === undefined) delete process.env.DATA_DIR;
  else process.env.DATA_DIR = originalDataDir;
});

const CLAUDE_SSE = [
  "event: message_start",
  'data: {"type":"message_start","message":{"id":"msg_probe","type":"message","role":"assistant","model":"claude-probe","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":15,"output_tokens":1}}}',
  "",
  "event: content_block_start",
  'data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}',
  "",
  "event: content_block_delta",
  'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"DOGFOOD_"}}',
  "",
  "event: content_block_delta",
  'data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"OK"}}',
  "",
  "event: content_block_stop",
  'data: {"type":"content_block_stop","index":0}',
  "",
  "event: message_delta",
  'data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":2}}',
  "",
  "event: message_stop",
  'data: {"type":"message_stop"}',
  "",
].join("\n");

const OPENAI_NO_DONE_SSE = [
  'data: {"id":"chatcmpl-probe","object":"chat.completion.chunk","created":1,"model":"m","choices":[{"index":0,"delta":{"role":"assistant","content":"DOGFOOD_"},"finish_reason":null}]}',
  "",
  'data: {"id":"chatcmpl-probe","object":"chat.completion.chunk","created":1,"model":"m","choices":[{"index":0,"delta":{"content":"OK"},"finish_reason":null}]}',
  "",
  'data: {"id":"chatcmpl-probe","object":"chat.completion.chunk","created":1,"model":"m","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":15,"completion_tokens":2,"total_tokens":17}}',
  "",
].join("\n");

const OPENAI_WITH_DONE_SSE = `${OPENAI_NO_DONE_SSE}${SENTINEL}\n\n`;

const GEMINI_SSE = [
  'data: {"candidates":[{"index":0,"content":{"parts":[{"text":"DOGFOOD_"}],"role":"model"}}],"modelVersion":"gemini-probe"}',
  "",
  'data: {"candidates":[{"index":0,"content":{"parts":[{"text":"OK"}],"role":"model"},"finishReason":"STOP"}],"modelVersion":"gemini-probe"}',
  "",
].join("\n");

function sseResponse(text) {
  const bytes = new TextEncoder().encode(text);
  return new Response(new ReadableStream({
    start(controller) { controller.enqueue(bytes); controller.close(); },
  }), { status: 200, headers: { "content-type": "text/event-stream" } });
}

async function streamViaWire({ provider, model, body, upstream, responseFormat, endpoint = "/v1/chat/completions", headers = { "content-type": "application/json" }, sourceFormatOverride = null }) {
  executorStub.execute.mockImplementation(async ({ body: sent }) => ({
    response: upstream,
    url: `https://probe.invalid/${provider}/v1/x`,
    headers: { "content-type": upstream.headers.get("content-type") },
    transformedBody: sent,
    responseFormat,
  }));

  const result = await handleChatCore({
    body,
    modelInfo: { provider, model },
    credentials: {},
    clientRawRequest: { endpoint, headers, body },
    stream: true,
    ...(sourceFormatOverride ? { sourceFormatOverride } : {}),
  });
  return result.response.text();
}

async function runPassthroughStream(provider, sourceFormat, input) {
  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    start(controller) { controller.enqueue(encoder.encode(input)); controller.close(); },
  });
  const reader = stream
    .pipeThrough(createPassthroughStreamWithLogger(provider, null, MODEL, null, null, null, null, sourceFormat))
    .getReader();
  const decoder = new TextDecoder();
  let text = "";
  for (;;) {
    const { value, done } = await reader.read();
    if (done) break;
    text += decoder.decode(value, { stream: true });
  }
  return text + decoder.decode();
}

const dataLines = (body) => body.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("data:"));
const jsonEvents = (body) => dataLines(body)
  .map((l) => l.slice(5).trim())
  .filter((p) => p && p !== "[DONE]")
  .map((p) => { try { return JSON.parse(p); } catch { return null; } })
  .filter(Boolean);
const claudeText = (body) => jsonEvents(body)
  .filter((e) => e.type === "content_block_delta" && e.delta?.type === "text_delta")
  .map((e) => e.delta.text).join("");
const openaiText = (body) => jsonEvents(body).map((e) => e.choices?.[0]?.delta?.content || "").join("");
const geminiText = (body) => jsonEvents(body)
  .flatMap((e) => e.candidates?.[0]?.content?.parts || []).map((p) => p.text || "").join("");

const anthropicBody = () => ({
  model: `anthropic/claude-sonnet-4-20250514`, max_tokens: 64,
  messages: [{ role: "user", content: "Reply with exactly: DOGFOOD_OK" }], stream: true,
});
const openaiBody = () => ({
  model: `openai/gpt-5.4`,
  messages: [{ role: "user", content: "Reply with exactly: DOGFOOD_OK" }], stream: true,
});
const geminiBody = () => ({
  model: `gemini/gemini-3.8-flash`,
  contents: [{ role: "user", parts: [{ text: "Reply with exactly: DOGFOOD_OK" }] }], stream: true,
});

describe("DF-9ROUTER-22 — passthrough [DONE] sentinel is OpenAI-client-only", () => {
  it("(a) emits NO synthetic sentinel for a Claude-format client and forwards the upstream message_stop", async () => {
    const body = await streamViaWire({
      provider: "anthropic", model: "claude-sonnet-4-20250514", body: anthropicBody(),
      upstream: sseResponse(CLAUDE_SSE), responseFormat: FORMATS.CLAUDE,
      endpoint: "/v1/messages",
      headers: { "content-type": "application/json", "anthropic-version": "2023-06-01" },
      sourceFormatOverride: FORMATS.CLAUDE,
    });
    expect(body).toContain("message_stop");
    expect(jsonEvents(body).some((e) => e.type === "message_stop")).toBe(true);
    expect(claudeText(body)).toBe("DOGFOOD_OK");
    expect(sentinelCount(body)).toBe(0);
    expect(body).not.toContain(SENTINEL);
  });

  it("(a2) the passthrough flush itself suppresses the sentinel for a Claude sourceFormat", async () => {
    const body = await runPassthroughStream("anthropic", FORMATS.CLAUDE, CLAUDE_SSE);
    expect(sentinelCount(body)).toBe(0);
    expect(body).toContain("message_stop");
    expect(claudeText(body)).toBe("DOGFOOD_OK");
  });

  it("(b) still emits exactly one sentinel for an OpenAI client when the upstream sends none", async () => {
    const body = await streamViaWire({
      provider: "openai", model: "gpt-5.4", body: openaiBody(),
      upstream: sseResponse(OPENAI_NO_DONE_SSE), responseFormat: FORMATS.OPENAI,
    });
    expect(sentinelCount(body)).toBe(1);
    expect(body.trimEnd().endsWith(SENTINEL)).toBe(true);
    expect(openaiText(body)).toBe("DOGFOOD_OK");
  });

  it("(c) does not duplicate the sentinel when an OpenAI passthrough upstream sends its own", async () => {
    const body = await streamViaWire({
      provider: "openai", model: "gpt-5.4", body: openaiBody(),
      upstream: sseResponse(OPENAI_WITH_DONE_SSE), responseFormat: FORMATS.OPENAI,
    });
    expect(sentinelCount(body)).toBe(1);
    expect(openaiText(body)).toBe("DOGFOOD_OK");
  });

  it("(d) emits no sentinel for a gemini-family passthrough provider", async () => {
    const body = await streamViaWire({
      provider: "gemini", model: "gemini-3.8-flash", body: geminiBody(),
      upstream: sseResponse(GEMINI_SSE), responseFormat: FORMATS.GEMINI,
      sourceFormatOverride: FORMATS.GEMINI,
    });
    expect(sentinelCount(body)).toBe(0);
    expect(geminiText(body)).toBe("DOGFOOD_OK");
  });

  it("(d2) the passthrough flush itself keeps the gemini-family gate", async () => {
    const body = await runPassthroughStream("gemini", FORMATS.GEMINI, GEMINI_SSE);
    expect(sentinelCount(body)).toBe(0);
    expect(geminiText(body)).toBe("DOGFOOD_OK");
  });
});

Verification

Install test deps (once) and run from tests/:

cd tests && npm install --legacy-peer-deps
npx vitest run --config ./vitest.config.js unit/passthrough-done-sentinel-gate.test.js

RED — before the fix (parent a99ebb73, test copied in). The Claude cases fail, proving the bug is real:

Test Files  1 failed (1)
     Tests  2 failed | 4 passed (6)
 FAIL  (a)  AssertionError: expected 1 to be +0
 FAIL  (a2) AssertionError: expected 1 to be +0

GREEN — after the fix (commit 2b8d4e2e):

Test Files  1 passed (1)
     Tests  6 passed (6)

The 4 required categories pass: Claude = 0 sentinels (message_stop forwarded), OpenAI no-upstream-[DONE] = 1, OpenAI with upstream [DONE] = 1 (exactly-once), gemini-family = 0. The gemini provider gate and the streamDoneSent latch are untouched, as is every TRANSLATE-mode path.

Evidence & signatures

# Evidence
- Problem class: sse-passthrough-terminator-per-client-format-gate
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-15T11:11:55.302Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "9router open-sse/utils/stream.js PASSTHROUGH flush emitted a synthetic 'data: [DONE]' for every client (OpenClaw anti-hang workaround) gated only on provider gemini-family \u2014 so Claude clients on a claude-native passthrough upstream received the upstream message_stop PLUS an extra [DONE], malformed per Anthropic contract. Fix: gate on sourceFormat != CLAUDE; plumb sourceFormat through createPassthroughStreamWithLogger (streamingHandler.js:44). Keep OpenAI exactly-once workaround, keep gemini gate. Regression suite must cover 4 cases incl. RED-proof. Verified fix: 9router commit 2b8d4e2e, judge PASS bf7d51ac.", "environment": "9router fork, open-sse streaming transform", "language": "javascript", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "sse-passthrough-terminator-per-client-format-gate", "provider": "openrouter", "solved_at": "2026-09-15T11:11:55.303Z", "version": ""}
Generated from the verified corpus · MIT licensedBack to the catalog