pi-freeflow 1.3.7 → 1.3.8
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/package.json +1 -1
- package/src/proxy.ts +16 -10
- package/src/stream-pipe.ts +64 -3
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-freeflow",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.3.
|
|
4
|
+
"version": "1.3.8",
|
|
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/proxy.ts
CHANGED
|
@@ -273,6 +273,7 @@ export function startProxy(
|
|
|
273
273
|
res,
|
|
274
274
|
req,
|
|
275
275
|
reqId,
|
|
276
|
+
relayPreview.url,
|
|
276
277
|
);
|
|
277
278
|
} else {
|
|
278
279
|
const data = await response.text();
|
|
@@ -328,6 +329,7 @@ export function startProxy(
|
|
|
328
329
|
res,
|
|
329
330
|
req,
|
|
330
331
|
reqId,
|
|
332
|
+
relayState.url,
|
|
331
333
|
);
|
|
332
334
|
} else {
|
|
333
335
|
const data = await response.text();
|
|
@@ -392,16 +394,20 @@ export function startProxy(
|
|
|
392
394
|
}
|
|
393
395
|
outHeaders["x-content-type-options"] = "nosniff";
|
|
394
396
|
res.writeHead(upstream.statusCode ?? 502, outHeaders);
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
397
|
+
if (isStream) {
|
|
398
|
+
pipeUpstreamStream(upstream, res, req, reqId, "direct");
|
|
399
|
+
} else {
|
|
400
|
+
upstream.on("error", (streamErr) => {
|
|
401
|
+
log(
|
|
402
|
+
"error",
|
|
403
|
+
"upstream stream error in direct proxy",
|
|
404
|
+
{ error: String(streamErr) },
|
|
405
|
+
reqId,
|
|
406
|
+
);
|
|
407
|
+
if (!res.writableEnded) res.end();
|
|
408
|
+
});
|
|
409
|
+
upstream.pipe(res);
|
|
410
|
+
}
|
|
405
411
|
},
|
|
406
412
|
);
|
|
407
413
|
|
package/src/stream-pipe.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { randomUUID } from "node:crypto";
|
|
|
9
9
|
import type * as http from "node:http";
|
|
10
10
|
import type { Readable } from "node:stream";
|
|
11
11
|
import { isDebugEnabled, log } from "./logger.ts";
|
|
12
|
-
|
|
12
|
+
import { markRelayFailure } from "./relay-state.ts";
|
|
13
13
|
/**
|
|
14
14
|
* Pipes an upstream readable stream to a client HTTP response.
|
|
15
15
|
*
|
|
@@ -23,6 +23,7 @@ export function pipeUpstreamStream(
|
|
|
23
23
|
res: http.ServerResponse,
|
|
24
24
|
req: http.IncomingMessage,
|
|
25
25
|
reqId?: string,
|
|
26
|
+
relayUrl?: string,
|
|
26
27
|
): void {
|
|
27
28
|
const rid = reqId || randomUUID().slice(0, 8);
|
|
28
29
|
let totalChunks = 0;
|
|
@@ -31,6 +32,8 @@ export function pipeUpstreamStream(
|
|
|
31
32
|
let thinkingBytes = 0;
|
|
32
33
|
let firstChunkAt: number | null = null;
|
|
33
34
|
const startAt = Date.now();
|
|
35
|
+
const isResponsesApi = req.url?.includes("/responses") ?? false;
|
|
36
|
+
let hasTerminalEvent = false;
|
|
34
37
|
|
|
35
38
|
const sniffThinking = (chunk: Buffer | string): boolean => {
|
|
36
39
|
const s =
|
|
@@ -47,12 +50,53 @@ export function pipeUpstreamStream(
|
|
|
47
50
|
);
|
|
48
51
|
};
|
|
49
52
|
|
|
53
|
+
const checkTerminalEvent = (s: string): boolean => {
|
|
54
|
+
return (
|
|
55
|
+
s.includes("response.completed") ||
|
|
56
|
+
s.includes("response.done") ||
|
|
57
|
+
s.includes("response.failed") ||
|
|
58
|
+
s.includes("response.incomplete") ||
|
|
59
|
+
s.includes("[DONE]")
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const ensureTerminalEvent = (isError = false, errorMsg?: string) => {
|
|
64
|
+
if (hasTerminalEvent || res.writableEnded) return;
|
|
65
|
+
if (relayUrl && relayUrl !== "direct") {
|
|
66
|
+
markRelayFailure(relayUrl, 0, errorMsg || "stream truncated prematurely");
|
|
67
|
+
}
|
|
68
|
+
if (isResponsesApi && totalChunks > 0) {
|
|
69
|
+
try {
|
|
70
|
+
if (isError) {
|
|
71
|
+
res.write(
|
|
72
|
+
`\nevent: response.failed\ndata: {"type":"response.failed","response":{"status":"failed","error":{"code":"stream_error","message":${JSON.stringify(errorMsg || "Upstream stream disconnected unexpectedly")}}}}\n\n`,
|
|
73
|
+
);
|
|
74
|
+
} else {
|
|
75
|
+
res.write(
|
|
76
|
+
`\nevent: response.incomplete\ndata: {"type":"response.incomplete","response":{"status":"incomplete","incomplete_details":{"reason":"cancelled"}}}\n\n`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
hasTerminalEvent = true;
|
|
80
|
+
log(
|
|
81
|
+
"warn",
|
|
82
|
+
`injected synthetic response.${isError ? "failed" : "incomplete"} for prematurely truncated stream`,
|
|
83
|
+
{ totalChunks, totalBytes, isError, errorMsg },
|
|
84
|
+
rid,
|
|
85
|
+
);
|
|
86
|
+
} catch {}
|
|
87
|
+
} else if (!isResponsesApi && totalChunks > 0) {
|
|
88
|
+
try {
|
|
89
|
+
res.write("\ndata: [DONE]\n\n");
|
|
90
|
+
hasTerminalEvent = true;
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
50
95
|
try {
|
|
51
96
|
if (typeof res.flushHeaders === "function") {
|
|
52
97
|
res.flushHeaders();
|
|
53
98
|
}
|
|
54
99
|
} catch {}
|
|
55
|
-
|
|
56
100
|
nodeStream.on("data", (chunk: Buffer | string) => {
|
|
57
101
|
try {
|
|
58
102
|
if (firstChunkAt === null) {
|
|
@@ -82,6 +126,14 @@ export function pipeUpstreamStream(
|
|
|
82
126
|
}
|
|
83
127
|
}
|
|
84
128
|
|
|
129
|
+
const strPreview =
|
|
130
|
+
typeof chunk === "string"
|
|
131
|
+
? chunk
|
|
132
|
+
: chunk.toString("utf8", 0, Math.min(chunk.length, 2000));
|
|
133
|
+
if (!hasTerminalEvent && checkTerminalEvent(strPreview)) {
|
|
134
|
+
hasTerminalEvent = true;
|
|
135
|
+
}
|
|
136
|
+
|
|
85
137
|
res.write(chunk);
|
|
86
138
|
const maybeFlush = res as unknown as { flush?: () => void };
|
|
87
139
|
if (typeof maybeFlush.flush === "function") {
|
|
@@ -91,15 +143,18 @@ export function pipeUpstreamStream(
|
|
|
91
143
|
});
|
|
92
144
|
|
|
93
145
|
nodeStream.on("error", (e: unknown) => {
|
|
146
|
+
const errorMsg = (e as Error)?.message || String(e);
|
|
94
147
|
log(
|
|
95
148
|
"error",
|
|
96
149
|
"upstream stream error",
|
|
97
|
-
{ error:
|
|
150
|
+
{ error: errorMsg, totalChunks, thinkingChunks, hasTerminalEvent },
|
|
98
151
|
rid,
|
|
99
152
|
);
|
|
100
153
|
try {
|
|
101
154
|
if (!res.headersSent) {
|
|
102
155
|
res.writeHead(502, { "content-type": "application/json" });
|
|
156
|
+
} else {
|
|
157
|
+
ensureTerminalEvent(true, errorMsg);
|
|
103
158
|
}
|
|
104
159
|
if (!res.writableEnded) {
|
|
105
160
|
res.end();
|
|
@@ -109,6 +164,9 @@ export function pipeUpstreamStream(
|
|
|
109
164
|
|
|
110
165
|
nodeStream.on("end", () => {
|
|
111
166
|
const elapsed = ((Date.now() - startAt) / 1000).toFixed(1);
|
|
167
|
+
if (!hasTerminalEvent && totalChunks > 0) {
|
|
168
|
+
ensureTerminalEvent(false);
|
|
169
|
+
}
|
|
112
170
|
if (thinkingChunks > 0) {
|
|
113
171
|
log(
|
|
114
172
|
"info",
|
|
@@ -131,6 +189,9 @@ export function pipeUpstreamStream(
|
|
|
131
189
|
|
|
132
190
|
nodeStream.on("close", () => {
|
|
133
191
|
try {
|
|
192
|
+
if (!hasTerminalEvent && totalChunks > 0) {
|
|
193
|
+
ensureTerminalEvent(true, "stream closed prematurely");
|
|
194
|
+
}
|
|
134
195
|
if (!res.writableEnded) res.end();
|
|
135
196
|
} catch {}
|
|
136
197
|
});
|