Merge branch 'master' of https://github.com/decolua/9router
Some checks failed
Deploy GitBook to 9router.github.io / build-deploy (push) Has been cancelled

# Conflicts:
#	open-sse/utils/usageTracking.js
This commit is contained in:
2026-06-29 10:10:13 +07:00
466 changed files with 28067 additions and 11945 deletions

View File

@@ -7,7 +7,10 @@ import { parseSSELine, hasValuableContent, fixInvalidId, formatSSE } from "./str
import { getOpenAIResponsesEventName, isOpenAIResponsesTerminalEvent, formatIncompleteOpenAIResponsesStreamFailure } from "./responsesStreamHelpers.js";
import { dbg, isDebugEnabled } from "./debugLog.js";
import { SSE_DONE, SSE_HEADERS, SSE_HEADERS_NO_BUFFER } from "./sseConstants.js";
export { COLORS, formatSSE };
export { SSE_DONE, SSE_HEADERS, SSE_HEADERS_NO_BUFFER };
// sharedEncoder is stateless — safe to share across streams
const sharedEncoder = new TextEncoder();
@@ -69,6 +72,7 @@ export function createSSEStream(options = {}) {
let currentOpenAIResponsesEvent = null;
let openAIResponsesTerminalSeen = false;
let openAIResponsesDoneSent = false;
let streamDoneSent = false; // track duplicate [DONE] across transform + flush
return new TransformStream({
transform(chunk, controller) {
@@ -164,7 +168,12 @@ export function createSSEStream(options = {}) {
output = `data: ${JSON.stringify(parsed)}\n`;
injectedUsage = true;
}
} catch { }
} catch {
// Skip non-JSON data lines silently — don't forward garbage to clients.
// Upstream providers sometimes return plain-text errors (HTML, rate-limit
// messages) in the SSE stream that would break downstream JSON decoders.
continue;
}
}
if (!injectedUsage) {
@@ -209,9 +218,10 @@ export function createSSEStream(options = {}) {
sseEmittedCount++;
}
const output = "data: [DONE]\n\n";
reqLogger?.appendConvertedChunk?.(output);
controller.enqueue(sharedEncoder.encode(output));
// [DONE] not emitted in translate mode — some clients' SSE decoders
// fail to parse the OpenAI sentinel on Claude-format translated streams.
// message_stop already signals end-of-response; stream close handles it.
streamDoneSent = true;
if (keepsOpenAIResponsesFormat) openAIResponsesDoneSent = true;
continue;
}
@@ -341,9 +351,11 @@ export function createSSEStream(options = {}) {
// Some clients (e.g. OpenClaw) expect the OpenAI-style sentinel:
// data: [DONE]\n\n
// Without it they can hang until timeout and trigger failover.
const doneOutput = "data: [DONE]\n\n";
reqLogger?.appendConvertedChunk?.(doneOutput);
controller.enqueue(sharedEncoder.encode(doneOutput));
if (!streamDoneSent) {
const doneOutput = "data: [DONE]\n\n";
reqLogger?.appendConvertedChunk?.(doneOutput);
controller.enqueue(sharedEncoder.encode(doneOutput));
}
if (onStreamComplete) {
onStreamComplete({
@@ -404,11 +416,8 @@ export function createSSEStream(options = {}) {
openAIResponsesTerminalSeen = true;
}
if (!keepsOpenAIResponsesFormat || !openAIResponsesDoneSent) {
const doneOutput = "data: [DONE]\n\n";
reqLogger?.appendConvertedChunk?.(doneOutput);
controller.enqueue(sharedEncoder.encode(doneOutput));
}
// [DONE] not emitted in translate mode — see comment above.
// Passthrough mode still emits it for standard OpenAI clients.
if (!hasValidUsage(state?.usage) && totalContentLength > 0) {
state.usage = estimateUsage(body, totalContentLength, sourceFormat);