Merge origin/master (v0.5.69) into gitea/new_feature
This commit is contained in:
@@ -49,7 +49,8 @@ export function createSSEStream(options = {}) {
|
||||
connectionId = null,
|
||||
body = null,
|
||||
onStreamComplete = null,
|
||||
apiKey = null
|
||||
apiKey = null,
|
||||
credentials = null
|
||||
} = options;
|
||||
|
||||
let buffer = "";
|
||||
@@ -59,7 +60,7 @@ export function createSSEStream(options = {}) {
|
||||
const decoder = new TextDecoder("utf-8", { fatal: false });
|
||||
|
||||
const state = mode === STREAM_MODE.TRANSLATE
|
||||
? { ...initState(sourceFormat), provider, toolNameMap, customToolNames: new Set(customToolNames || []), model }
|
||||
? { ...initState(sourceFormat), provider, toolNameMap, customToolNames: new Set(customToolNames || []), model, sessionId: credentials?._clientSessionId || null }
|
||||
: null;
|
||||
|
||||
let totalContentLength = 0;
|
||||
@@ -75,6 +76,35 @@ export function createSSEStream(options = {}) {
|
||||
let openAIResponsesTerminalSeen = false;
|
||||
let openAIResponsesDoneSent = false;
|
||||
let streamDoneSent = false; // track duplicate [DONE] across transform + flush
|
||||
let finalized = false;
|
||||
|
||||
// Usage/logging tail, callable from transform() as well as flush(): a client that
|
||||
// closes right after the terminal event cancels the reader, and flush() never runs.
|
||||
const finalizeStream = () => {
|
||||
if (finalized) return;
|
||||
finalized = true;
|
||||
|
||||
const isPassthrough = mode === STREAM_MODE.PASSTHROUGH;
|
||||
let finalUsage = isPassthrough ? usage : state?.usage;
|
||||
|
||||
if (!hasValidUsage(finalUsage) && totalContentLength > 0) {
|
||||
finalUsage = estimateUsage(body, totalContentLength, isPassthrough ? FORMATS.OPENAI : sourceFormat);
|
||||
if (isPassthrough) usage = finalUsage; else state.usage = finalUsage;
|
||||
}
|
||||
|
||||
if (hasValidUsage(finalUsage)) {
|
||||
logUsage(isPassthrough ? provider : (state?.provider || targetFormat), finalUsage, model, connectionId, apiKey);
|
||||
} else {
|
||||
appendRequestLog({ model, provider, connectionId, tokens: null, status: "200 OK" }).catch(() => { });
|
||||
}
|
||||
|
||||
if (onStreamComplete) {
|
||||
onStreamComplete({
|
||||
content: accumulatedContent,
|
||||
thinking: accumulatedThinking
|
||||
}, finalUsage, ttftAt);
|
||||
}
|
||||
};
|
||||
|
||||
return new TransformStream({
|
||||
transform(chunk, controller) {
|
||||
@@ -105,6 +135,7 @@ export function createSSEStream(options = {}) {
|
||||
if (mode === STREAM_MODE.PASSTHROUGH) {
|
||||
let output;
|
||||
let injectedUsage = false;
|
||||
let responsesTerminal = false;
|
||||
|
||||
if (trimmed.startsWith("data:") && trimmed.slice(5).trim() !== "[DONE]") {
|
||||
try {
|
||||
@@ -168,6 +199,8 @@ export function createSSEStream(options = {}) {
|
||||
usage = mergeUsage(usage, extracted);
|
||||
}
|
||||
|
||||
responsesTerminal = isOpenAIResponsesTerminalEvent(currentOpenAIResponsesEvent, parsed);
|
||||
|
||||
const isFinishChunk = parsed.choices?.[0]?.finish_reason;
|
||||
if (isFinishChunk && !hasValidUsage(parsed.usage)) {
|
||||
const estimated = estimateUsage(body, totalContentLength, FORMATS.OPENAI);
|
||||
@@ -202,6 +235,8 @@ export function createSSEStream(options = {}) {
|
||||
|
||||
reqLogger?.appendConvertedChunk?.(output);
|
||||
controller.enqueue(sharedEncoder.encode(output));
|
||||
// Responses clients (codex CLI) close on response.completed instead of [DONE]
|
||||
if (responsesTerminal) finalizeStream();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -292,6 +327,8 @@ export function createSSEStream(options = {}) {
|
||||
controller.enqueue(sharedEncoder.encode(output));
|
||||
currentOpenAIResponsesEvent = null;
|
||||
sseEmittedCount++;
|
||||
// Responses clients (codex) close on response.completed instead of [DONE]
|
||||
if (openAIResponsesTerminalSeen) finalizeStream();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -355,13 +392,6 @@ export function createSSEStream(options = {}) {
|
||||
controller.enqueue(sharedEncoder.encode(output));
|
||||
}
|
||||
|
||||
if (!hasValidUsage(usage) && totalContentLength > 0) {
|
||||
usage = estimateUsage(body, totalContentLength, FORMATS.OPENAI);
|
||||
}
|
||||
|
||||
if (hasValidUsage(usage)) {
|
||||
logUsage(provider, usage, model, connectionId, apiKey);
|
||||
}
|
||||
// IMPORTANT: In passthrough mode we still must terminate the SSE stream.
|
||||
// Some clients (e.g. OpenClaw) expect the OpenAI-style sentinel:
|
||||
// data: [DONE]\n\n
|
||||
@@ -374,18 +404,26 @@ export function createSSEStream(options = {}) {
|
||||
controller.enqueue(sharedEncoder.encode(doneOutput));
|
||||
}
|
||||
|
||||
if (onStreamComplete) {
|
||||
onStreamComplete({
|
||||
content: accumulatedContent,
|
||||
thinking: accumulatedThinking
|
||||
}, usage, ttftAt);
|
||||
}
|
||||
finalizeStream();
|
||||
return;
|
||||
}
|
||||
|
||||
if (buffer.trim()) {
|
||||
const parsed = parseSSELine(buffer.trim());
|
||||
if (parsed && !parsed.done) {
|
||||
// Same parse as the transform loop: without targetFormat this only
|
||||
// accepts "data: " lines, so an NDJSON provider (Ollama) lost whatever
|
||||
// arrived without its closing newline.
|
||||
const parsed = parseSSELine(buffer.trim(), targetFormat);
|
||||
// parseSSELine turns the SSE sentinel "data: [DONE]" into { done: true },
|
||||
// which must not be translated. An Ollama chunk also carries done:true,
|
||||
// but it is the real final chunk — it holds finish_reason and the token
|
||||
// counts — so it has to go through.
|
||||
const isDoneSentinel = parsed?.done && targetFormat !== FORMATS.OLLAMA;
|
||||
if (parsed && !isDoneSentinel) {
|
||||
// Same accumulation the transform loop does, so finalizeStream() can
|
||||
// log a tail chunk's tokens instead of falling back to null.
|
||||
const extracted = extractUsage(parsed);
|
||||
if (extracted) state.usage = mergeUsage(state.usage, extracted);
|
||||
|
||||
const translated = translateResponse(targetFormat, sourceFormat, parsed, state);
|
||||
|
||||
if (translated?._openaiIntermediate) {
|
||||
@@ -441,26 +479,16 @@ export function createSSEStream(options = {}) {
|
||||
streamDoneSent = true;
|
||||
}
|
||||
|
||||
if (!hasValidUsage(state?.usage) && totalContentLength > 0) {
|
||||
state.usage = estimateUsage(body, totalContentLength, sourceFormat);
|
||||
}
|
||||
if (hasValidUsage(state?.usage)) {
|
||||
logUsage(state.provider || targetFormat, state.usage, model, connectionId, apiKey);
|
||||
}
|
||||
if (onStreamComplete) {
|
||||
onStreamComplete({
|
||||
content: accumulatedContent,
|
||||
thinking: accumulatedThinking
|
||||
}, state?.usage, ttftAt);
|
||||
}
|
||||
finalizeStream();
|
||||
} catch (error) {
|
||||
console.log("Error in flush:", error);
|
||||
finalizeStream();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function createSSETransformStreamWithLogger(targetFormat, sourceFormat, provider = null, reqLogger = null, toolNameMap = null, model = null, connectionId = null, body = null, onStreamComplete = null, apiKey = null, customToolNames = null) {
|
||||
export function createSSETransformStreamWithLogger(targetFormat, sourceFormat, provider = null, reqLogger = null, toolNameMap = null, model = null, connectionId = null, body = null, onStreamComplete = null, apiKey = null, customToolNames = null, credentials = null) {
|
||||
return createSSEStream({
|
||||
mode: STREAM_MODE.TRANSLATE,
|
||||
targetFormat,
|
||||
@@ -473,7 +501,8 @@ export function createSSETransformStreamWithLogger(targetFormat, sourceFormat, p
|
||||
connectionId,
|
||||
body,
|
||||
onStreamComplete,
|
||||
apiKey
|
||||
apiKey,
|
||||
credentials
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user