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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-freeflow",
3
3
  "type": "module",
4
- "version": "1.3.7",
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
- upstream.on("error", (streamErr) => {
396
- log(
397
- "error",
398
- "upstream stream error in direct proxy",
399
- { error: String(streamErr) },
400
- reqId,
401
- );
402
- if (!res.writableEnded) res.end();
403
- });
404
- upstream.pipe(res);
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
 
@@ -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: String(e), totalChunks, thinkingChunks },
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
  });