pi-freeflow 1.1.8 → 1.2.0
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/extensions/index.ts +72 -41
- package/package.json +1 -1
package/extensions/index.ts
CHANGED
|
@@ -735,13 +735,37 @@ const LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
|
735
735
|
const LOG_MAX_FILES = 3;
|
|
736
736
|
const DEBUG_STATE_FILE = path.join(homedir(), ".pi", "agent", "pi-freeflow-debug.json");
|
|
737
737
|
interface DebugState { debug: boolean; level?: LogLevel }
|
|
738
|
+
let cachedDebugState: DebugState | null | undefined = undefined;
|
|
739
|
+
let cachedDebugMtime = 0;
|
|
740
|
+
let cachedDebugAt = 0;
|
|
738
741
|
function loadDebugState(): DebugState | null {
|
|
742
|
+
const now = Date.now();
|
|
743
|
+
// cache for 1s to avoid per-chunk FS hit in hot pipe path
|
|
744
|
+
if (cachedDebugState !== undefined && now - cachedDebugAt < 1000) {
|
|
745
|
+
return cachedDebugState;
|
|
746
|
+
}
|
|
739
747
|
try {
|
|
740
|
-
if (!fs.existsSync(DEBUG_STATE_FILE))
|
|
748
|
+
if (!fs.existsSync(DEBUG_STATE_FILE)) {
|
|
749
|
+
cachedDebugState = null;
|
|
750
|
+
cachedDebugAt = now;
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
const stat = fs.statSync(DEBUG_STATE_FILE);
|
|
754
|
+
if (stat.mtimeMs === cachedDebugMtime && cachedDebugState !== undefined) {
|
|
755
|
+
cachedDebugAt = now;
|
|
756
|
+
return cachedDebugState;
|
|
757
|
+
}
|
|
741
758
|
const raw = fs.readFileSync(DEBUG_STATE_FILE, "utf8");
|
|
742
759
|
const d = JSON.parse(raw) as DebugState;
|
|
743
|
-
if (typeof d.debug === "boolean")
|
|
760
|
+
if (typeof d.debug === "boolean") {
|
|
761
|
+
cachedDebugState = d;
|
|
762
|
+
cachedDebugMtime = stat.mtimeMs;
|
|
763
|
+
cachedDebugAt = now;
|
|
764
|
+
return d;
|
|
765
|
+
}
|
|
744
766
|
} catch {}
|
|
767
|
+
cachedDebugState = null;
|
|
768
|
+
cachedDebugAt = now;
|
|
745
769
|
return null;
|
|
746
770
|
}
|
|
747
771
|
function saveDebugState(s: DebugState): void {
|
|
@@ -751,6 +775,16 @@ function saveDebugState(s: DebugState): void {
|
|
|
751
775
|
const tmp = `${DEBUG_STATE_FILE}.${randomUUID()}.tmp`;
|
|
752
776
|
fs.writeFileSync(tmp, JSON.stringify(s, null, 2), "utf8");
|
|
753
777
|
fs.renameSync(tmp, DEBUG_STATE_FILE);
|
|
778
|
+
// update cache
|
|
779
|
+
try {
|
|
780
|
+
const stat = fs.statSync(DEBUG_STATE_FILE);
|
|
781
|
+
cachedDebugState = s;
|
|
782
|
+
cachedDebugMtime = stat.mtimeMs;
|
|
783
|
+
cachedDebugAt = Date.now();
|
|
784
|
+
} catch {
|
|
785
|
+
cachedDebugState = s;
|
|
786
|
+
cachedDebugAt = Date.now();
|
|
787
|
+
}
|
|
754
788
|
} catch {}
|
|
755
789
|
}
|
|
756
790
|
function getMinLogLevel(): number {
|
|
@@ -1419,8 +1453,8 @@ function startProxy(
|
|
|
1419
1453
|
} catch {}
|
|
1420
1454
|
|
|
1421
1455
|
const upstream: Upstream = isKilo ? "kilo" : "opencode";
|
|
1456
|
+
const isStream = parsedBody?.stream === true;
|
|
1422
1457
|
if (!checkRateLimit(clientIP, upstream)) {
|
|
1423
|
-
log("warn", `rate limit hit for ${upstream} from ${clientIP}`, { ip: clientIP, upstream }, reqId);
|
|
1424
1458
|
res.writeHead(429, { "content-type": "application/json" });
|
|
1425
1459
|
res.end(JSON.stringify({ error: "rate limit exceeded" }));
|
|
1426
1460
|
return;
|
|
@@ -1474,52 +1508,49 @@ function startProxy(
|
|
|
1474
1508
|
activeHost,
|
|
1475
1509
|
);
|
|
1476
1510
|
try {
|
|
1477
|
-
if (
|
|
1511
|
+
if (parsedBody) {
|
|
1478
1512
|
const relayBodyObj = structuredClone(parsedBody);
|
|
1479
1513
|
normalizeRequestBody(relayBodyObj, true, isKilo, reqId);
|
|
1480
|
-
relayBody = Buffer.from(JSON.stringify(relayBodyObj));
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
res.end(data);
|
|
1514
|
+
const relayBody = Buffer.from(JSON.stringify(relayBodyObj));
|
|
1515
|
+
const response = await relayFetch(fullUrl, {
|
|
1516
|
+
method: req.method || "POST",
|
|
1517
|
+
headers: relayHeaders,
|
|
1518
|
+
body: relayBody,
|
|
1519
|
+
signal: AbortSignal.timeout(300_000),
|
|
1520
|
+
}, reqId);
|
|
1521
|
+
if (isStream && response.ok && response.body) {
|
|
1522
|
+
const ct =
|
|
1523
|
+
response.headers.get("content-type") || "text/event-stream";
|
|
1524
|
+
res.writeHead(response.status, {
|
|
1525
|
+
"content-type": ct,
|
|
1526
|
+
"cache-control": "no-cache, no-transform",
|
|
1527
|
+
"connection": "keep-alive",
|
|
1528
|
+
"x-accel-buffering": "no",
|
|
1529
|
+
});
|
|
1530
|
+
pipeUpstreamStream(
|
|
1531
|
+
Readable.fromWeb(
|
|
1532
|
+
response.body as unknown as WebReadableStream,
|
|
1533
|
+
),
|
|
1534
|
+
res,
|
|
1535
|
+
req,
|
|
1536
|
+
reqId,
|
|
1537
|
+
);
|
|
1538
|
+
} else {
|
|
1539
|
+
const data = await response.text();
|
|
1540
|
+
const ct =
|
|
1541
|
+
response.headers.get("content-type") || "application/json";
|
|
1542
|
+
res.writeHead(response.status, { "content-type": ct });
|
|
1543
|
+
res.end(data);
|
|
1544
|
+
}
|
|
1545
|
+
return; // relay handled the response
|
|
1513
1546
|
}
|
|
1514
|
-
return; // relay handled the response
|
|
1515
1547
|
} catch (e) {
|
|
1516
1548
|
log("warn", "opencode relay failed, falling back to direct", {
|
|
1517
1549
|
error: String(e),
|
|
1518
1550
|
}, reqId);
|
|
1519
|
-
|
|
1520
|
-
|
|
1551
|
+
if (res.headersSent) return; // can't recover mid-stream
|
|
1552
|
+
}
|
|
1521
1553
|
}
|
|
1522
|
-
// direct path (existing, untouched)
|
|
1523
1554
|
// direct path — with debug trace and thinking-aware normalize
|
|
1524
1555
|
let directBody = Buffer.concat(bodyChunks);
|
|
1525
1556
|
if (parsedBody) {
|