pi-freeflow 1.32.1 → 1.32.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/stream-pipe.ts +37 -56
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.32.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Prevent JSON parse errors on interrupted responses: network packet boundaries split mid-delta no longer produce unterminated string syntax errors when a turn is retried or terminated.
|
|
8
|
+
|
|
3
9
|
## 1.32.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-freeflow",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.32.
|
|
4
|
+
"version": "1.32.2",
|
|
5
5
|
"description": "Thin provider for OMP/Pi — model list + dumb relay proxy + log; host pi-ai owns thinking/normalization",
|
|
6
6
|
"main": "extensions/index.ts",
|
|
7
7
|
"types": "src/index.ts",
|
package/src/stream-pipe.ts
CHANGED
|
@@ -745,6 +745,8 @@ export function pipeUpstreamStream(
|
|
|
745
745
|
const cut = nodeStream;
|
|
746
746
|
upstreamEnded = false;
|
|
747
747
|
nodeStream = next;
|
|
748
|
+
sseBuffer = "";
|
|
749
|
+
terminalScanCarry = Buffer.alloc(0);
|
|
748
750
|
watch(next);
|
|
749
751
|
try {
|
|
750
752
|
if (!cut.destroyed) cut.destroy();
|
|
@@ -805,11 +807,11 @@ export function pipeUpstreamStream(
|
|
|
805
807
|
try {
|
|
806
808
|
if (isError) {
|
|
807
809
|
res.write(
|
|
808
|
-
`\nevent: response.failed\ndata: {"type":"response.failed","response":{"status":"failed","error":{"code":"stream_error","message":${JSON.stringify(errorMsg || "Upstream stream disconnected unexpectedly")}}}}\n\n`,
|
|
810
|
+
`\n\nevent: response.failed\ndata: {"type":"response.failed","response":{"status":"failed","error":{"code":"stream_error","message":${JSON.stringify(errorMsg || "Upstream stream disconnected unexpectedly")}}}}\n\n`,
|
|
809
811
|
);
|
|
810
812
|
} else {
|
|
811
813
|
res.write(
|
|
812
|
-
`\nevent: response.incomplete\ndata: {"type":"response.incomplete","response":{"status":"incomplete","incomplete_details":{"reason":"cancelled"}}}\n\n`,
|
|
814
|
+
`\n\nevent: response.incomplete\ndata: {"type":"response.incomplete","response":{"status":"incomplete","incomplete_details":{"reason":"cancelled"}}}\n\n`,
|
|
813
815
|
);
|
|
814
816
|
}
|
|
815
817
|
hasTerminalEvent = true;
|
|
@@ -822,7 +824,7 @@ export function pipeUpstreamStream(
|
|
|
822
824
|
} catch { }
|
|
823
825
|
} else if (!isResponsesApi && totalChunks > 0) {
|
|
824
826
|
try {
|
|
825
|
-
res.write("\ndata: [DONE]\n\n");
|
|
827
|
+
res.write("\n\ndata: [DONE]\n\n");
|
|
826
828
|
hasTerminalEvent = true;
|
|
827
829
|
} catch { }
|
|
828
830
|
}
|
|
@@ -860,6 +862,8 @@ export function pipeUpstreamStream(
|
|
|
860
862
|
}
|
|
861
863
|
upstreamEnded = false;
|
|
862
864
|
nodeStream = next;
|
|
865
|
+
sseBuffer = "";
|
|
866
|
+
terminalScanCarry = Buffer.alloc(0);
|
|
863
867
|
watch(next);
|
|
864
868
|
log("debug", "mid-stream refire: continuing turn on replacement stream", { totalChunks, totalBytes }, rid);
|
|
865
869
|
return true;
|
|
@@ -902,59 +906,22 @@ export function pipeUpstreamStream(
|
|
|
902
906
|
);
|
|
903
907
|
}
|
|
904
908
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
909
|
+
// Buffer across chunks into complete SSE blocks delimited by double-newline.
|
|
910
|
+
// Forwarding only complete blocks guarantees that network chunk boundaries
|
|
911
|
+
// never split JSON strings on the wire or leave unterminated syntax when
|
|
912
|
+
// mid-stream refire or truncation occurs.
|
|
913
|
+
sseBuffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
914
|
+
let boundary = sseBoundary.exec(sseBuffer);
|
|
915
|
+
while (boundary) {
|
|
916
|
+
const block = sseBuffer.slice(0, boundary.index);
|
|
917
|
+
sseBuffer = sseBuffer.slice(boundary.index + boundary[0].length);
|
|
918
|
+
if (cloakState && cloak) {
|
|
913
919
|
const rewritten = rewriteSseBlock(block, cloakState, cloak);
|
|
914
920
|
if (rewritten !== null) forwardText(`${rewritten}\n\n`);
|
|
915
|
-
|
|
921
|
+
} else {
|
|
922
|
+
forwardText(`${block}\n\n`);
|
|
916
923
|
}
|
|
917
|
-
|
|
918
|
-
}
|
|
919
|
-
// Always scan the FULL chunk plus the small carry from the previous
|
|
920
|
-
// view. SSE chunks are KBs at most, so includes() over everything
|
|
921
|
-
// is negligible against correctness — the removed head/tail windows
|
|
922
|
-
// are exactly what let markers buried mid-chunk or split across
|
|
923
|
-
// adjacent chunks escape and cause duplicate synthetic terminals.
|
|
924
|
-
const buf =
|
|
925
|
-
typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk;
|
|
926
|
-
const scanBuf =
|
|
927
|
-
terminalScanCarry.length > 0
|
|
928
|
-
? Buffer.concat([terminalScanCarry, buf])
|
|
929
|
-
: buf;
|
|
930
|
-
const scanned = scanBuf.toString("utf8");
|
|
931
|
-
if (!sawToolCall && TOOL_CALL_MARKERS.some((m) => scanned.includes(m))) sawToolCall = true;
|
|
932
|
-
if (!hasTerminalEvent && checkTerminalEvent(scanned)) {
|
|
933
|
-
// Upstream-issued transient model failure on a text-only turn: hold the
|
|
934
|
-
// frame for one silent resume instead of terminating (tool turns veto
|
|
935
|
-
// inside holdModelFailureForRefire and keep the terminal below).
|
|
936
|
-
if (isTransientModelFailureFrame(scanned) && holdModelFailureForRefire(chunk)) return;
|
|
937
|
-
hasTerminalEvent = true;
|
|
938
|
-
}
|
|
939
|
-
terminalScanCarry = Buffer.from(scanBuf.subarray(Math.max(0, scanBuf.length - 32)));
|
|
940
|
-
|
|
941
|
-
const canContinue = res.write(chunk);
|
|
942
|
-
if (canContinue === false && !nodeStream.destroyed) {
|
|
943
|
-
// Backpressure: pause the upstream source until the client
|
|
944
|
-
// socket drains, instead of buffering an unbounded amount into
|
|
945
|
-
// the response. 'drain' might never fire if the client closes —
|
|
946
|
-
// the close handlers below destroy the stream regardless of
|
|
947
|
-
// its paused state.
|
|
948
|
-
nodeStream.pause();
|
|
949
|
-
const onDrain = () => {
|
|
950
|
-
res.off("drain", onDrain);
|
|
951
|
-
if (!nodeStream.destroyed) nodeStream.resume();
|
|
952
|
-
};
|
|
953
|
-
res.once("drain", onDrain);
|
|
954
|
-
}
|
|
955
|
-
const maybeFlush = res as unknown as { flush?: () => void };
|
|
956
|
-
if (typeof maybeFlush.flush === "function") {
|
|
957
|
-
maybeFlush.flush();
|
|
924
|
+
boundary = sseBoundary.exec(sseBuffer);
|
|
958
925
|
}
|
|
959
926
|
} catch { }
|
|
960
927
|
};
|
|
@@ -985,9 +952,13 @@ export function pipeUpstreamStream(
|
|
|
985
952
|
res.writeHead(502, { "content-type": "application/json" });
|
|
986
953
|
} else if (isInternalAbort) {
|
|
987
954
|
clientAborted = true;
|
|
955
|
+
sseBuffer = "";
|
|
956
|
+
terminalScanCarry = Buffer.alloc(0);
|
|
988
957
|
ensureTerminalEvent(false, errorMsg || "stream interrupted", false);
|
|
989
958
|
} else {
|
|
990
959
|
if (await tryRefire()) return;
|
|
960
|
+
sseBuffer = "";
|
|
961
|
+
terminalScanCarry = Buffer.alloc(0);
|
|
991
962
|
ensureTerminalEvent(!isSubstantial(totalChunks, totalBytes), errorMsg, true);
|
|
992
963
|
}
|
|
993
964
|
if (!res.writableEnded) {
|
|
@@ -1003,11 +974,17 @@ export function pipeUpstreamStream(
|
|
|
1003
974
|
upstreamEnded = true;
|
|
1004
975
|
// Cloaked streams hold the trailing incomplete block: flush it (rewritten)
|
|
1005
976
|
// ahead of the terminal-marker check so a coalesced final event still counts.
|
|
1006
|
-
if (
|
|
977
|
+
if (sseBuffer.trim() !== "") {
|
|
1007
978
|
const tail = sseBuffer;
|
|
1008
979
|
sseBuffer = "";
|
|
1009
|
-
|
|
1010
|
-
|
|
980
|
+
if (checkTerminalEvent(tail)) {
|
|
981
|
+
if (cloakState && cloak) {
|
|
982
|
+
const rewritten = rewriteSseBlock(tail, cloakState, cloak);
|
|
983
|
+
if (rewritten !== null) forwardText(`${rewritten}\n\n`);
|
|
984
|
+
} else {
|
|
985
|
+
forwardText(tail);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
1011
988
|
}
|
|
1012
989
|
if (hasTerminalEvent) recordOnce(false);
|
|
1013
990
|
const elapsed = ((Date.now() - startAt) / 1000).toFixed(1);
|
|
@@ -1054,11 +1031,15 @@ export function pipeUpstreamStream(
|
|
|
1054
1031
|
if (clientAborted || upstreamEnded) {
|
|
1055
1032
|
// Client disconnect teardown or post-end cleanup — the relay
|
|
1056
1033
|
// is not at fault; still give the host a terminal event.
|
|
1034
|
+
sseBuffer = "";
|
|
1035
|
+
terminalScanCarry = Buffer.alloc(0);
|
|
1057
1036
|
ensureTerminalEvent(false, "stream interrupted by client", false);
|
|
1058
1037
|
} else {
|
|
1059
1038
|
// Same offer as the clean-end path above: one silent re-fire before
|
|
1060
1039
|
// marking this relay and terminating the turn.
|
|
1061
1040
|
if (await tryRefire()) return;
|
|
1041
|
+
sseBuffer = "";
|
|
1042
|
+
terminalScanCarry = Buffer.alloc(0);
|
|
1062
1043
|
// Upstream socket died mid-stream with no error event.
|
|
1063
1044
|
// For muse-spark large payloads: raxtant 514KB failed but feoni 802KB
|
|
1064
1045
|
// succeeded with same 2.6MB in — so this is edge-specific, not pure
|