pi-freeflow 1.1.7 → 1.1.9
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 +405 -78
- package/package.json +1 -1
package/extensions/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import https from "node:https";
|
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { Readable } from "node:stream";
|
|
13
|
+
import type { ReadableStream as WebReadableStream } from "node:stream/web";
|
|
13
14
|
import { fileURLToPath } from "node:url";
|
|
14
15
|
import type { ExtensionAPI, ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
15
16
|
|
|
@@ -174,8 +175,11 @@ let aliveCatalog: RegisteredModel[] = [];
|
|
|
174
175
|
async function relayFetch(
|
|
175
176
|
url: string,
|
|
176
177
|
opts: RequestInit = {},
|
|
178
|
+
reqId?: string,
|
|
177
179
|
): Promise<Response> {
|
|
180
|
+
const rid = reqId || randomUUID().slice(0, 8);
|
|
178
181
|
if (!relayState.enabled) {
|
|
182
|
+
log("debug", `relayFetch: direct (relay disabled) -> ${url}`, undefined, rid);
|
|
179
183
|
return fetch(url, opts);
|
|
180
184
|
}
|
|
181
185
|
|
|
@@ -193,7 +197,22 @@ async function relayFetch(
|
|
|
193
197
|
? (opts.body.length / 1024).toFixed(1)
|
|
194
198
|
: "0";
|
|
195
199
|
|
|
196
|
-
log("info", `request starting (${bodySizeKB}KB payload) -> ${url}
|
|
200
|
+
log("info", `request starting (${bodySizeKB}KB payload) -> ${url}`, undefined, rid);
|
|
201
|
+
if (isDebugEnabled()) {
|
|
202
|
+
try {
|
|
203
|
+
const bodyPreview = typeof opts.body === "string" ? opts.body.slice(0, 1200) : "";
|
|
204
|
+
const modelMatch = bodyPreview.match(/"model"\s*:\s*"([^"]+)"/);
|
|
205
|
+
const streamMatch = bodyPreview.match(/"stream"\s*:\s*(true|false)/);
|
|
206
|
+
log("debug", `request detail`, {
|
|
207
|
+
model: modelMatch?.[1],
|
|
208
|
+
stream: streamMatch?.[1],
|
|
209
|
+
sizeKB: bodySizeKB,
|
|
210
|
+
relayTarget,
|
|
211
|
+
relayPath,
|
|
212
|
+
candidates: candidates.length,
|
|
213
|
+
}, rid);
|
|
214
|
+
} catch {}
|
|
215
|
+
}
|
|
197
216
|
|
|
198
217
|
for (let i = 0; i < candidates.length; i++) {
|
|
199
218
|
const targetUrl = candidates[i];
|
|
@@ -207,6 +226,7 @@ async function relayFetch(
|
|
|
207
226
|
headers.set("x-relay-target", relayTarget);
|
|
208
227
|
headers.set("x-relay-path", relayPath);
|
|
209
228
|
headers.set("host", targetHost);
|
|
229
|
+
headers.set("x-request-id", rid);
|
|
210
230
|
const signal = opts.signal || AbortSignal.timeout(300_000);
|
|
211
231
|
const res = await fetch(targetUrl, { ...opts, headers, signal });
|
|
212
232
|
const elapsed = ((Date.now() - attemptStart) / 1000).toFixed(1);
|
|
@@ -218,6 +238,7 @@ async function relayFetch(
|
|
|
218
238
|
"warn",
|
|
219
239
|
`relay ${targetUrl} hit HTTP 504 Gateway Timeout in ${elapsed}s (prompt evaluation exceeded Vercel 25s limit) — fast fallback to direct upstream`,
|
|
220
240
|
{ upstream: url, sizeKB: bodySizeKB },
|
|
241
|
+
rid,
|
|
221
242
|
);
|
|
222
243
|
break;
|
|
223
244
|
}
|
|
@@ -228,6 +249,7 @@ async function relayFetch(
|
|
|
228
249
|
"warn",
|
|
229
250
|
`relay ${targetUrl} returned HTTP ${res.status} in ${elapsed}s — rolling to next relay`,
|
|
230
251
|
{ upstream: url, status: res.status },
|
|
252
|
+
rid,
|
|
231
253
|
);
|
|
232
254
|
continue;
|
|
233
255
|
}
|
|
@@ -237,12 +259,19 @@ async function relayFetch(
|
|
|
237
259
|
if (relayState.url !== targetUrl) {
|
|
238
260
|
log("info", `active relay auto-switched to ${targetUrl}`, {
|
|
239
261
|
previous: relayState.url,
|
|
240
|
-
});
|
|
262
|
+
}, rid);
|
|
241
263
|
relayState.url = targetUrl;
|
|
242
264
|
saveRelayState(relayState);
|
|
243
265
|
}
|
|
244
266
|
|
|
245
|
-
log("info", `relay ${targetUrl} succeeded (HTTP ${res.status} in ${elapsed}s)
|
|
267
|
+
log("info", `relay ${targetUrl} succeeded (HTTP ${res.status} in ${elapsed}s)`, undefined, rid);
|
|
268
|
+
if (isDebugEnabled()) {
|
|
269
|
+
log("debug", `relay headers`, {
|
|
270
|
+
status: res.status,
|
|
271
|
+
contentType: res.headers.get("content-type"),
|
|
272
|
+
via: res.headers.get("via") || res.headers.get("x-vercel-id") || "direct",
|
|
273
|
+
}, rid);
|
|
274
|
+
}
|
|
246
275
|
|
|
247
276
|
// Update TUI status
|
|
248
277
|
const label = shortRelayLabel(targetUrl);
|
|
@@ -261,6 +290,7 @@ async function relayFetch(
|
|
|
261
290
|
"warn",
|
|
262
291
|
`relay ${targetUrl} fetch error in ${elapsed}s — rolling to next relay`,
|
|
263
292
|
{ upstream: url, error: String(err) },
|
|
293
|
+
rid,
|
|
264
294
|
);
|
|
265
295
|
continue;
|
|
266
296
|
}
|
|
@@ -272,21 +302,22 @@ async function relayFetch(
|
|
|
272
302
|
log("warn", "relays bypassed/exhausted — attempting direct fetch to upstream", {
|
|
273
303
|
upstream: url,
|
|
274
304
|
sizeKB: bodySizeKB,
|
|
275
|
-
});
|
|
305
|
+
}, rid);
|
|
276
306
|
const directHeaders = new Headers(opts.headers);
|
|
277
307
|
directHeaders.delete("x-relay-target");
|
|
278
308
|
directHeaders.delete("x-relay-path");
|
|
279
309
|
directHeaders.set("host", u.host);
|
|
310
|
+
directHeaders.set("x-request-id", rid);
|
|
280
311
|
const directRes = await fetch(url, { ...opts, headers: directHeaders });
|
|
281
312
|
const directElapsed = ((Date.now() - directStart) / 1000).toFixed(1);
|
|
282
|
-
log("info", `direct fetch returned HTTP ${directRes.status} in ${directElapsed}s
|
|
313
|
+
log("info", `direct fetch returned HTTP ${directRes.status} in ${directElapsed}s`, undefined, rid);
|
|
283
314
|
return directRes;
|
|
284
315
|
} catch (directErr) {
|
|
285
316
|
const directElapsed = ((Date.now() - directStart) / 1000).toFixed(1);
|
|
286
317
|
log("error", `direct fallback also failed in ${directElapsed}s`, {
|
|
287
318
|
upstream: url,
|
|
288
319
|
error: String(directErr),
|
|
289
|
-
});
|
|
320
|
+
}, rid);
|
|
290
321
|
if (lastResponse) return lastResponse;
|
|
291
322
|
throw directErr || lastError;
|
|
292
323
|
}
|
|
@@ -686,23 +717,153 @@ const STRIP_HEADERS = new Set([
|
|
|
686
717
|
"proxy-authorization",
|
|
687
718
|
]);
|
|
688
719
|
|
|
689
|
-
// ── Logger
|
|
690
|
-
|
|
691
|
-
|
|
720
|
+
// ── Logger (structured, leveled, rotating, request-aware) ─────────
|
|
721
|
+
// Reference: pi-ai SDK diagnostics (provider streaming + thinking deltas)
|
|
722
|
+
// - api/openai-completions.js: thinkingFormat branches + reasoning_effort mapping
|
|
723
|
+
// - api/openai-responses-shared.js: reasoning block handling for Responses API
|
|
724
|
+
// - api/anthropic-messages.js: thinking/thinking_delta + signature handling
|
|
725
|
+
// This logger mirrors that trace plane for offline audit without TUI noise.
|
|
726
|
+
type LogLevel = "debug" | "info" | "warn" | "error" | "audit";
|
|
727
|
+
const LOG_LEVEL_ORDER: Record<LogLevel, number> = {
|
|
728
|
+
debug: 0,
|
|
729
|
+
info: 1,
|
|
730
|
+
warn: 2,
|
|
731
|
+
error: 3,
|
|
732
|
+
audit: 4,
|
|
733
|
+
};
|
|
734
|
+
const LOG_MAX_BYTES = 5 * 1024 * 1024;
|
|
735
|
+
const LOG_MAX_FILES = 3;
|
|
736
|
+
const DEBUG_STATE_FILE = path.join(homedir(), ".pi", "agent", "pi-freeflow-debug.json");
|
|
737
|
+
interface DebugState { debug: boolean; level?: LogLevel }
|
|
738
|
+
let cachedDebugState: DebugState | null | undefined = undefined;
|
|
739
|
+
let cachedDebugMtime = 0;
|
|
740
|
+
let cachedDebugAt = 0;
|
|
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
|
+
}
|
|
692
747
|
try {
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
+
}
|
|
758
|
+
const raw = fs.readFileSync(DEBUG_STATE_FILE, "utf8");
|
|
759
|
+
const d = JSON.parse(raw) as DebugState;
|
|
760
|
+
if (typeof d.debug === "boolean") {
|
|
761
|
+
cachedDebugState = d;
|
|
762
|
+
cachedDebugMtime = stat.mtimeMs;
|
|
763
|
+
cachedDebugAt = now;
|
|
764
|
+
return d;
|
|
765
|
+
}
|
|
766
|
+
} catch {}
|
|
767
|
+
cachedDebugState = null;
|
|
768
|
+
cachedDebugAt = now;
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
function saveDebugState(s: DebugState): void {
|
|
772
|
+
try {
|
|
773
|
+
const dir = path.dirname(DEBUG_STATE_FILE);
|
|
774
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
775
|
+
const tmp = `${DEBUG_STATE_FILE}.${randomUUID()}.tmp`;
|
|
776
|
+
fs.writeFileSync(tmp, JSON.stringify(s, null, 2), "utf8");
|
|
777
|
+
fs.renameSync(tmp, DEBUG_STATE_FILE);
|
|
778
|
+
// update cache
|
|
696
779
|
try {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
+
}
|
|
788
|
+
} catch {}
|
|
789
|
+
}
|
|
790
|
+
function getMinLogLevel(): number {
|
|
791
|
+
const dbg = loadDebugState();
|
|
792
|
+
if (dbg?.debug) return LOG_LEVEL_ORDER.debug;
|
|
793
|
+
if (dbg?.level && dbg.level in LOG_LEVEL_ORDER) return LOG_LEVEL_ORDER[dbg.level];
|
|
794
|
+
const raw = (
|
|
795
|
+
process.env.FREEFLOW_LOG_LEVEL ||
|
|
796
|
+
process.env.BANSOS_LOG_LEVEL ||
|
|
797
|
+
"info"
|
|
798
|
+
).toLowerCase();
|
|
799
|
+
if (raw in LOG_LEVEL_ORDER) return LOG_LEVEL_ORDER[raw as LogLevel];
|
|
800
|
+
if (process.env.FREEFLOW_DEBUG === "1" || process.env.FREEFLOW_DEBUG === "true") {
|
|
801
|
+
return LOG_LEVEL_ORDER.debug;
|
|
802
|
+
}
|
|
803
|
+
return LOG_LEVEL_ORDER.info;
|
|
804
|
+
}
|
|
805
|
+
function shouldLog(level: LogLevel): boolean {
|
|
806
|
+
return LOG_LEVEL_ORDER[level] >= getMinLogLevel();
|
|
807
|
+
}
|
|
808
|
+
function isDebugEnabled(): boolean {
|
|
809
|
+
return LOG_LEVEL_ORDER.debug >= getMinLogLevel();
|
|
810
|
+
}
|
|
811
|
+
function rotateLogsIfNeeded(): void {
|
|
812
|
+
try {
|
|
813
|
+
if (!fs.existsSync(LOG_FILE)) return;
|
|
814
|
+
if (fs.statSync(LOG_FILE).size <= LOG_MAX_BYTES) return;
|
|
815
|
+
for (let i = LOG_MAX_FILES - 1; i >= 1; i--) {
|
|
816
|
+
const src = i === 1 ? LOG_FILE : `${LOG_FILE}.${i - 1}`;
|
|
817
|
+
const dst = `${LOG_FILE}.${i}`;
|
|
818
|
+
try {
|
|
819
|
+
if (fs.existsSync(src)) {
|
|
820
|
+
if (fs.existsSync(dst)) fs.unlinkSync(dst);
|
|
821
|
+
fs.renameSync(src, dst);
|
|
822
|
+
}
|
|
823
|
+
} catch {}
|
|
824
|
+
}
|
|
825
|
+
} catch {}
|
|
826
|
+
}
|
|
827
|
+
function formatLogMeta(
|
|
828
|
+
meta?: Record<string, unknown>,
|
|
829
|
+
reqId?: string,
|
|
830
|
+
): string {
|
|
831
|
+
const parts: string[] = [];
|
|
832
|
+
if (reqId) parts.push(`req=${reqId}`);
|
|
833
|
+
if (meta && Object.keys(meta).length > 0) {
|
|
834
|
+
const safe: Record<string, unknown> = {};
|
|
835
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
836
|
+
if (typeof v === "string" && v.length > 800) {
|
|
837
|
+
safe[k] = `${v.slice(0, 800)}…(${v.length})`;
|
|
838
|
+
} else {
|
|
839
|
+
safe[k] = v;
|
|
701
840
|
}
|
|
702
|
-
}
|
|
841
|
+
}
|
|
842
|
+
parts.push(JSON.stringify(safe));
|
|
843
|
+
}
|
|
844
|
+
return parts.length ? ` ${parts.join(" ")}` : "";
|
|
845
|
+
}
|
|
846
|
+
function log(
|
|
847
|
+
level: LogLevel,
|
|
848
|
+
message: string,
|
|
849
|
+
meta?: Record<string, unknown>,
|
|
850
|
+
reqId?: string,
|
|
851
|
+
): void {
|
|
852
|
+
if (!shouldLog(level)) return;
|
|
853
|
+
try {
|
|
854
|
+
const ts = new Date().toISOString();
|
|
855
|
+
const line = `[${ts}] [${level.toUpperCase()}]${reqId ? ` [${reqId}]` : ""} ${message}${formatLogMeta(meta, undefined)}\n`;
|
|
856
|
+
rotateLogsIfNeeded();
|
|
703
857
|
fs.appendFileSync(LOG_FILE, line, "utf8");
|
|
704
858
|
} catch {}
|
|
705
859
|
}
|
|
860
|
+
function logDebug(
|
|
861
|
+
message: string,
|
|
862
|
+
meta?: Record<string, unknown>,
|
|
863
|
+
reqId?: string,
|
|
864
|
+
): void {
|
|
865
|
+
log("debug", message, meta, reqId);
|
|
866
|
+
}
|
|
706
867
|
|
|
707
868
|
// ── Rate Limiter ───────────────────────────────────────────────────
|
|
708
869
|
// Kilo documents 200 free requests/hour/IP. OpenCode owns its own daily quota;
|
|
@@ -984,8 +1145,24 @@ function normalizeRequestBody(
|
|
|
984
1145
|
body: Record<string, unknown>,
|
|
985
1146
|
isRelay = false,
|
|
986
1147
|
isKilo = false,
|
|
1148
|
+
reqId?: string,
|
|
987
1149
|
): Record<string, unknown> {
|
|
988
|
-
|
|
1150
|
+
const DBG = isDebugEnabled();
|
|
1151
|
+
const modelId = typeof body.model === "string" ? body.model : "";
|
|
1152
|
+
const modelDef = MODEL_MAP.get(modelId);
|
|
1153
|
+
const isResponsesApi = modelId === "muse-spark-1.2-contributor-free";
|
|
1154
|
+
|
|
1155
|
+
if (DBG) {
|
|
1156
|
+
log("debug", `normalize: incoming model=${modelId} kilo=${isKilo} relay=${isRelay}`, {
|
|
1157
|
+
reasoning_effort: body.reasoning_effort,
|
|
1158
|
+
reasoning: body.reasoning,
|
|
1159
|
+
thinking: (body as Record<string, unknown>).thinking,
|
|
1160
|
+
tool_choice: body.tool_choice,
|
|
1161
|
+
toolsLen: Array.isArray(body.tools) ? body.tools.length : undefined,
|
|
1162
|
+
}, reqId);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// 1. Tool choice & empty tools normalization (pi-ai compat: opencode only supports auto)
|
|
989
1166
|
if (Array.isArray(body.tools) && body.tools.length === 0) {
|
|
990
1167
|
delete body.tools;
|
|
991
1168
|
delete body.tool_choice;
|
|
@@ -994,26 +1171,57 @@ function normalizeRequestBody(
|
|
|
994
1171
|
delete body.tool_choice;
|
|
995
1172
|
delete body.tools;
|
|
996
1173
|
} else if (!isKilo && body.tool_choice && body.tool_choice !== "auto") {
|
|
997
|
-
// OpenCode Zen only supports "auto" or undefined
|
|
998
1174
|
body.tool_choice = "auto";
|
|
999
1175
|
}
|
|
1000
1176
|
|
|
1001
|
-
//
|
|
1002
|
-
|
|
1003
|
-
|
|
1177
|
+
// 1b. Anthropic thinking -> OpenAI reasoning_effort auto-translate
|
|
1178
|
+
// Pi sends anthropic `thinking: {type:"enabled",budget_tokens}` when provider is anthropic.
|
|
1179
|
+
// Our proxy is always openai-completions/responses upstream, so translate.
|
|
1180
|
+
// Ref: pi-ai api/anthropic-messages.js (thinking.type adaptive/enabled/disabled) -> api/openai-completions.js (reasoning_effort)
|
|
1181
|
+
const thinkingRaw = (body as Record<string, unknown>).thinking;
|
|
1182
|
+
if (thinkingRaw && typeof thinkingRaw === "object") {
|
|
1183
|
+
const th = thinkingRaw as Record<string, unknown>;
|
|
1184
|
+
if (th.type === "disabled") {
|
|
1185
|
+
delete (body as Record<string, unknown>).thinking;
|
|
1186
|
+
// Mark as off so downstream reasoning mapping can clear effort
|
|
1187
|
+
if (!body.reasoning_effort && !body.reasoning) {
|
|
1188
|
+
body.reasoning_effort = "off";
|
|
1189
|
+
}
|
|
1190
|
+
} else if (th.type === "enabled" || th.type === "adaptive") {
|
|
1191
|
+
delete (body as Record<string, unknown>).thinking;
|
|
1192
|
+
// Preserve budget as hint if no explicit effort set
|
|
1193
|
+
if (!body.reasoning_effort && typeof th.budget_tokens === "number") {
|
|
1194
|
+
const budget = th.budget_tokens as number;
|
|
1195
|
+
if (budget >= 8000) body.reasoning_effort = "xhigh";
|
|
1196
|
+
else if (budget >= 4000) body.reasoning_effort = "high";
|
|
1197
|
+
else if (budget >= 2000) body.reasoning_effort = "medium";
|
|
1198
|
+
else body.reasoning_effort = "low";
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// 2. Reasoning normalization — per-model thinkingLevelMap aware
|
|
1204
|
+
// Ref: pi-ai api/openai-completions.js (compat.thinkingFormat branches) + api/openai-responses-shared.js
|
|
1205
|
+
const mapEffort = (rawEffort: string): string | null | undefined => {
|
|
1206
|
+
const key = rawEffort.toLowerCase() as keyof NonNullable<ModelDef["thinkingLevelMap"]>;
|
|
1207
|
+
if (modelDef?.thinkingLevelMap && key in modelDef.thinkingLevelMap) {
|
|
1208
|
+
return modelDef.thinkingLevelMap[key] as string | null;
|
|
1209
|
+
}
|
|
1210
|
+
if (rawEffort === "xhigh" || rawEffort === "max") {
|
|
1211
|
+
return isResponsesApi ? "xhigh" : (modelId === "x-preview-f-free" ? "max" : "xhigh");
|
|
1212
|
+
}
|
|
1213
|
+
if (rawEffort === "high" || rawEffort === "medium") return "high";
|
|
1214
|
+
if (rawEffort === "minimal") return "minimal";
|
|
1215
|
+
if (rawEffort === "none" || rawEffort === "off") return null;
|
|
1216
|
+
return "low";
|
|
1217
|
+
};
|
|
1004
1218
|
|
|
1005
1219
|
if (typeof body.reasoning_effort === "string") {
|
|
1006
|
-
const
|
|
1007
|
-
if (
|
|
1008
|
-
body.reasoning_effort = isResponsesApi ? "xhigh" : (modelId === "x-preview-f-free" ? "max" : "xhigh");
|
|
1009
|
-
} else if (re === "high" || re === "medium") {
|
|
1010
|
-
body.reasoning_effort = "high";
|
|
1011
|
-
} else if (re === "minimal") {
|
|
1012
|
-
body.reasoning_effort = "minimal";
|
|
1013
|
-
} else if (re === "none" || re === "off") {
|
|
1220
|
+
const mapped = mapEffort(body.reasoning_effort);
|
|
1221
|
+
if (mapped === null || mapped === undefined) {
|
|
1014
1222
|
delete body.reasoning_effort;
|
|
1015
1223
|
} else {
|
|
1016
|
-
body.reasoning_effort =
|
|
1224
|
+
body.reasoning_effort = mapped;
|
|
1017
1225
|
}
|
|
1018
1226
|
}
|
|
1019
1227
|
if (body.reasoning && typeof body.reasoning === "object") {
|
|
@@ -1021,21 +1229,17 @@ function normalizeRequestBody(
|
|
|
1021
1229
|
if (r.effort === "none" || r.effort === "off") {
|
|
1022
1230
|
delete r.effort;
|
|
1023
1231
|
} else if (typeof r.effort === "string") {
|
|
1024
|
-
const
|
|
1025
|
-
if (
|
|
1026
|
-
r.effort
|
|
1027
|
-
} else if (re === "high" || re === "medium") {
|
|
1028
|
-
r.effort = "high";
|
|
1029
|
-
} else if (re === "minimal") {
|
|
1030
|
-
r.effort = "minimal";
|
|
1232
|
+
const mapped = mapEffort(r.effort);
|
|
1233
|
+
if (mapped === null || mapped === undefined) {
|
|
1234
|
+
delete r.effort;
|
|
1031
1235
|
} else {
|
|
1032
|
-
r.effort =
|
|
1236
|
+
r.effort = mapped;
|
|
1033
1237
|
}
|
|
1034
1238
|
}
|
|
1239
|
+
if (isResponsesApi && r.effort === "max") r.effort = "xhigh";
|
|
1035
1240
|
}
|
|
1036
1241
|
|
|
1037
1242
|
// 3. Max & Min tokens clamping (model-specific clamp + Vercel relay clamp)
|
|
1038
|
-
const modelDef = MODEL_MAP.get(modelId);
|
|
1039
1243
|
const modelMax = modelDef?.maxTokens ?? RELAY_MAX_TOKENS;
|
|
1040
1244
|
const clampTokens = (val: number): number => {
|
|
1041
1245
|
let clamped = val;
|
|
@@ -1055,16 +1259,47 @@ function normalizeRequestBody(
|
|
|
1055
1259
|
body.max_output_tokens = clampTokens(body.max_output_tokens);
|
|
1056
1260
|
}
|
|
1057
1261
|
|
|
1262
|
+
if (DBG) {
|
|
1263
|
+
log("debug", `normalize: outgoing model=${modelId}`, {
|
|
1264
|
+
reasoning_effort: body.reasoning_effort,
|
|
1265
|
+
reasoning: body.reasoning,
|
|
1266
|
+
max_tokens: body.max_tokens,
|
|
1267
|
+
max_output_tokens: body.max_output_tokens,
|
|
1268
|
+
}, reqId);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1058
1271
|
return body;
|
|
1059
1272
|
}
|
|
1060
1273
|
|
|
1061
1274
|
// ponytail: shared stream pipe — upstream abort/timeout must end response,
|
|
1062
1275
|
// not become an uncaught exception that crashes pi.
|
|
1276
|
+
// Adds lightweight thinking-tag sniffing for debug audit (no payload mutation).
|
|
1277
|
+
// Ref: pi-ai api/openai-completions.js (thinkingDelta) + api/openai-responses-shared.js (reasoning)
|
|
1063
1278
|
function pipeUpstreamStream(
|
|
1064
1279
|
nodeStream: Readable,
|
|
1065
1280
|
res: http.ServerResponse,
|
|
1066
1281
|
req: http.IncomingMessage,
|
|
1282
|
+
reqId?: string,
|
|
1067
1283
|
): void {
|
|
1284
|
+
const rid = reqId || randomUUID().slice(0, 8);
|
|
1285
|
+
let totalChunks = 0;
|
|
1286
|
+
let totalBytes = 0;
|
|
1287
|
+
let thinkingChunks = 0;
|
|
1288
|
+
let thinkingBytes = 0;
|
|
1289
|
+
let firstChunkAt: number | null = null;
|
|
1290
|
+
const startAt = Date.now();
|
|
1291
|
+
const sniffThinking = (chunk: Buffer | string): boolean => {
|
|
1292
|
+
const s = typeof chunk === "string" ? chunk : chunk.toString("utf8", 0, Math.min(chunk.length, 4000));
|
|
1293
|
+
return (
|
|
1294
|
+
s.includes("reasoning") ||
|
|
1295
|
+
s.includes("thinking") ||
|
|
1296
|
+
s.includes("<think>") ||
|
|
1297
|
+
s.includes("reasoning_content") ||
|
|
1298
|
+
s.includes("\"type\":\"thinking\"") ||
|
|
1299
|
+
s.includes("thinking_delta")
|
|
1300
|
+
);
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1068
1303
|
try {
|
|
1069
1304
|
if (typeof res.flushHeaders === "function") {
|
|
1070
1305
|
res.flushHeaders();
|
|
@@ -1073,15 +1308,29 @@ function pipeUpstreamStream(
|
|
|
1073
1308
|
|
|
1074
1309
|
nodeStream.on("data", (chunk: Buffer | string) => {
|
|
1075
1310
|
try {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1311
|
+
if (firstChunkAt === null) {
|
|
1312
|
+
firstChunkAt = Date.now();
|
|
1313
|
+
const ttfb = firstChunkAt - startAt;
|
|
1314
|
+
log("debug", `stream first chunk in ${ttfb}ms`, undefined, rid);
|
|
1079
1315
|
}
|
|
1316
|
+
totalChunks++;
|
|
1317
|
+
totalBytes += typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.length;
|
|
1318
|
+
if (sniffThinking(chunk)) {
|
|
1319
|
+
thinkingChunks++;
|
|
1320
|
+
thinkingBytes += typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.length;
|
|
1321
|
+
if (isDebugEnabled() && thinkingChunks <= 3) {
|
|
1322
|
+
const preview = typeof chunk === "string" ? chunk.slice(0, 600) : chunk.toString("utf8", 0, 600);
|
|
1323
|
+
log("debug", `thinking chunk #${thinkingChunks}`, { preview: preview.slice(0, 400) }, rid);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
res.write(chunk);
|
|
1327
|
+
const maybeFlush = res as unknown as { flush?: () => void };
|
|
1328
|
+
if (typeof maybeFlush.flush === "function") maybeFlush.flush();
|
|
1080
1329
|
} catch {}
|
|
1081
1330
|
});
|
|
1082
1331
|
|
|
1083
1332
|
nodeStream.on("error", (e: unknown) => {
|
|
1084
|
-
log("error", "upstream stream error", { error: String(e) });
|
|
1333
|
+
log("error", "upstream stream error", { error: String(e), totalChunks, thinkingChunks }, rid);
|
|
1085
1334
|
try {
|
|
1086
1335
|
if (!res.headersSent) {
|
|
1087
1336
|
res.writeHead(502, { "content-type": "application/json" });
|
|
@@ -1092,6 +1341,12 @@ function pipeUpstreamStream(
|
|
|
1092
1341
|
} catch {}
|
|
1093
1342
|
});
|
|
1094
1343
|
nodeStream.on("end", () => {
|
|
1344
|
+
const elapsed = ((Date.now() - startAt) / 1000).toFixed(1);
|
|
1345
|
+
if (thinkingChunks > 0) {
|
|
1346
|
+
log("info", `stream ended in ${elapsed}s — ${totalChunks} chunks (${(totalBytes/1024).toFixed(1)}KB), thinking: ${thinkingChunks} chunks (${(thinkingBytes/1024).toFixed(1)}KB)`, undefined, rid);
|
|
1347
|
+
} else if (isDebugEnabled()) {
|
|
1348
|
+
log("debug", `stream ended in ${elapsed}s — ${totalChunks} chunks (${(totalBytes/1024).toFixed(1)}KB), no thinking detected`, undefined, rid);
|
|
1349
|
+
}
|
|
1095
1350
|
try {
|
|
1096
1351
|
if (!res.writableEnded) res.end();
|
|
1097
1352
|
} catch {}
|
|
@@ -1102,6 +1357,7 @@ function pipeUpstreamStream(
|
|
|
1102
1357
|
} catch {}
|
|
1103
1358
|
});
|
|
1104
1359
|
req.on("aborted", () => {
|
|
1360
|
+
log("warn", "client aborted — destroying upstream", { totalChunks }, rid);
|
|
1105
1361
|
if (!nodeStream.destroyed) nodeStream.destroy();
|
|
1106
1362
|
});
|
|
1107
1363
|
req.on("close", () => {
|
|
@@ -1123,7 +1379,10 @@ function startProxy(
|
|
|
1123
1379
|
|
|
1124
1380
|
const server = http.createServer((req, res) => {
|
|
1125
1381
|
const clientIP = getClientIP(req);
|
|
1126
|
-
|
|
1382
|
+
const reqId = randomUUID().slice(0, 8);
|
|
1383
|
+
if (isDebugEnabled()) {
|
|
1384
|
+
log("debug", `incoming ${req.method} ${req.url} from ${clientIP}`, { ip: clientIP, method: req.method, url: req.url }, reqId);
|
|
1385
|
+
}
|
|
1127
1386
|
if (!ALLOWED_METHODS.has(req.method ?? "")) {
|
|
1128
1387
|
res.writeHead(405, { "content-type": "application/json" });
|
|
1129
1388
|
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
@@ -1173,7 +1432,7 @@ function startProxy(
|
|
|
1173
1432
|
// Read body to detect model for routing
|
|
1174
1433
|
const bodyChunks: Buffer[] = [];
|
|
1175
1434
|
req.on("error", (err) => {
|
|
1176
|
-
log("warn", "client request error during body buffering", { error: String(err) });
|
|
1435
|
+
log("warn", "client request error during body buffering", { error: String(err) }, reqId);
|
|
1177
1436
|
if (!res.headersSent) res.writeHead(400, { "content-type": "application/json" });
|
|
1178
1437
|
res.end(JSON.stringify({ error: "bad request" }));
|
|
1179
1438
|
});
|
|
@@ -1194,13 +1453,17 @@ function startProxy(
|
|
|
1194
1453
|
} catch {}
|
|
1195
1454
|
|
|
1196
1455
|
const upstream: Upstream = isKilo ? "kilo" : "opencode";
|
|
1456
|
+
if (!checkRateLimit(clientIP, upstream)) {
|
|
1457
|
+
log("warn", `rate limit hit for ${upstream} from ${clientIP}`, { ip: clientIP, upstream }, reqId);
|
|
1458
|
+
res.writeHead(429, { "content-type": "application/json" });
|
|
1459
|
+
res.end(JSON.stringify({ error: "rate limit exceeded" }));
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1197
1462
|
|
|
1198
1463
|
try {
|
|
1199
1464
|
if (isKilo && parsedBody) {
|
|
1200
|
-
// KiloCode gateway routing (free models are keyless)
|
|
1201
|
-
const isStream = parsedBody.stream === true;
|
|
1202
1465
|
const kiloBodyObj = structuredClone(parsedBody);
|
|
1203
|
-
normalizeRequestBody(kiloBodyObj, true, isKilo);
|
|
1466
|
+
normalizeRequestBody(kiloBodyObj, true, isKilo, reqId);
|
|
1204
1467
|
const response = await relayFetch(KILO_CHAT_URL, {
|
|
1205
1468
|
method: "POST",
|
|
1206
1469
|
headers: {
|
|
@@ -1209,7 +1472,7 @@ function startProxy(
|
|
|
1209
1472
|
},
|
|
1210
1473
|
body: JSON.stringify(kiloBodyObj),
|
|
1211
1474
|
signal: AbortSignal.timeout(300_000),
|
|
1212
|
-
});
|
|
1475
|
+
}, reqId);
|
|
1213
1476
|
if (isStream && response.ok && response.body) {
|
|
1214
1477
|
const ct =
|
|
1215
1478
|
response.headers.get("content-type") || "text/event-stream";
|
|
@@ -1221,10 +1484,11 @@ function startProxy(
|
|
|
1221
1484
|
});
|
|
1222
1485
|
pipeUpstreamStream(
|
|
1223
1486
|
Readable.fromWeb(
|
|
1224
|
-
response.body as unknown as
|
|
1487
|
+
response.body as unknown as WebReadableStream,
|
|
1225
1488
|
),
|
|
1226
1489
|
res,
|
|
1227
1490
|
req,
|
|
1491
|
+
reqId,
|
|
1228
1492
|
);
|
|
1229
1493
|
} else {
|
|
1230
1494
|
const data = await response.text();
|
|
@@ -1244,12 +1508,9 @@ function startProxy(
|
|
|
1244
1508
|
activeHost,
|
|
1245
1509
|
);
|
|
1246
1510
|
try {
|
|
1247
|
-
let relayBody = bodyChunks.length
|
|
1248
|
-
? Buffer.concat(bodyChunks)
|
|
1249
|
-
: undefined;
|
|
1250
1511
|
if (relayBody && parsedBody) {
|
|
1251
1512
|
const relayBodyObj = structuredClone(parsedBody);
|
|
1252
|
-
normalizeRequestBody(relayBodyObj, true, isKilo);
|
|
1513
|
+
normalizeRequestBody(relayBodyObj, true, isKilo, reqId);
|
|
1253
1514
|
relayBody = Buffer.from(JSON.stringify(relayBodyObj));
|
|
1254
1515
|
}
|
|
1255
1516
|
const response = await relayFetch(fullUrl, {
|
|
@@ -1257,7 +1518,7 @@ function startProxy(
|
|
|
1257
1518
|
headers: relayHeaders,
|
|
1258
1519
|
body: relayBody,
|
|
1259
1520
|
signal: AbortSignal.timeout(300_000),
|
|
1260
|
-
});
|
|
1521
|
+
}, reqId);
|
|
1261
1522
|
const ct =
|
|
1262
1523
|
response.headers.get("content-type") || "application/json";
|
|
1263
1524
|
if (response.ok && response.body) {
|
|
@@ -1271,10 +1532,11 @@ function startProxy(
|
|
|
1271
1532
|
});
|
|
1272
1533
|
pipeUpstreamStream(
|
|
1273
1534
|
Readable.fromWeb(
|
|
1274
|
-
response.body as unknown as
|
|
1535
|
+
response.body as unknown as WebReadableStream,
|
|
1275
1536
|
),
|
|
1276
1537
|
res,
|
|
1277
1538
|
req,
|
|
1539
|
+
reqId,
|
|
1278
1540
|
);
|
|
1279
1541
|
} else {
|
|
1280
1542
|
const data = await response.text();
|
|
@@ -1286,19 +1548,23 @@ function startProxy(
|
|
|
1286
1548
|
return; // relay handled the response
|
|
1287
1549
|
} catch (e) {
|
|
1288
1550
|
log("warn", "opencode relay failed, falling back to direct", {
|
|
1289
|
-
|
|
1290
|
-
|
|
1551
|
+
error: String(e),
|
|
1552
|
+
}, reqId);
|
|
1291
1553
|
if (res.headersSent) return; // can't recover mid-stream
|
|
1292
1554
|
}
|
|
1293
1555
|
}
|
|
1294
1556
|
// direct path (existing, untouched)
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1557
|
+
// direct path — with debug trace and thinking-aware normalize
|
|
1558
|
+
let directBody = Buffer.concat(bodyChunks);
|
|
1559
|
+
if (parsedBody) {
|
|
1560
|
+
const directBodyObj = structuredClone(parsedBody);
|
|
1561
|
+
normalizeRequestBody(directBodyObj, false, isKilo, reqId);
|
|
1562
|
+
directBody = Buffer.from(JSON.stringify(directBodyObj));
|
|
1563
|
+
}
|
|
1564
|
+
if (isDebugEnabled()) {
|
|
1565
|
+
log("debug", `direct upstream ${target.hostname}${target.pathname} (${directBody.length}B)`, { model: parsedBody?.model, isKilo }, reqId);
|
|
1566
|
+
}
|
|
1567
|
+
const fwd = sanitizeHeaders(req.headers, target.hostname);
|
|
1302
1568
|
if (directBody.length > 0) {
|
|
1303
1569
|
fwd["content-length"] = String(directBody.byteLength);
|
|
1304
1570
|
}
|
|
@@ -1323,14 +1589,14 @@ function startProxy(
|
|
|
1323
1589
|
outHeaders["x-content-type-options"] = "nosniff";
|
|
1324
1590
|
res.writeHead(upstream.statusCode ?? 502, outHeaders);
|
|
1325
1591
|
upstream.on("error", (streamErr) => {
|
|
1326
|
-
log("error", "upstream stream error in direct proxy", { error: String(streamErr) });
|
|
1592
|
+
log("error", "upstream stream error in direct proxy", { error: String(streamErr) }, reqId);
|
|
1327
1593
|
if (!res.writableEnded) res.end();
|
|
1328
1594
|
});
|
|
1329
1595
|
upstream.pipe(res);
|
|
1330
1596
|
},
|
|
1331
1597
|
);
|
|
1332
1598
|
proxy.on("error", (proxyErr) => {
|
|
1333
|
-
log("error", "proxy socket error", { error: String(proxyErr) });
|
|
1599
|
+
log("error", "proxy socket error", { error: String(proxyErr) }, reqId);
|
|
1334
1600
|
if (!res.headersSent) {
|
|
1335
1601
|
res.writeHead(502, { "content-type": "application/json" });
|
|
1336
1602
|
res.end(JSON.stringify({ error: "upstream error" }));
|
|
@@ -1355,7 +1621,7 @@ function startProxy(
|
|
|
1355
1621
|
proxy.end(directBody);
|
|
1356
1622
|
}
|
|
1357
1623
|
} catch (err) {
|
|
1358
|
-
log("error", "proxy error", { error: String(err) });
|
|
1624
|
+
log("error", "proxy error", { error: String(err) }, reqId);
|
|
1359
1625
|
if (!res.headersSent)
|
|
1360
1626
|
res.writeHead(502, { "content-type": "application/json" });
|
|
1361
1627
|
res.end(JSON.stringify({ error: "internal error" }));
|
|
@@ -1480,12 +1746,12 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1480
1746
|
|
|
1481
1747
|
pi.registerProvider("freeflow", providerConfig);
|
|
1482
1748
|
}
|
|
1483
|
-
// ── /bansos command:
|
|
1749
|
+
// ── /bansos command: relay + debug + logs ───
|
|
1484
1750
|
const commandSpec = {
|
|
1485
1751
|
description:
|
|
1486
|
-
"Relay egress: on | off | status | logs | url [URL] | deploy | list | use <URL> | remove <URL> | refresh",
|
|
1752
|
+
"Relay egress: on | off | status | logs [level] [n] | debug on|off|status | url [URL] | deploy | list | use <URL> | remove <URL> | refresh",
|
|
1487
1753
|
getArgumentCompletions: (prefix: string) =>
|
|
1488
|
-
["on", "off", "status", "url", "deploy", "list", "use", "remove", "refresh", "models"]
|
|
1754
|
+
["on", "off", "status", "url", "deploy", "list", "use", "remove", "refresh", "models", "logs", "debug", "trace"]
|
|
1489
1755
|
.filter((s) => s.startsWith(prefix))
|
|
1490
1756
|
.map((s) => ({ value: s, label: s })),
|
|
1491
1757
|
handler: async (args: string, ctx) => {
|
|
@@ -1623,15 +1889,76 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1623
1889
|
setRelay(true, url, "manual");
|
|
1624
1890
|
persist();
|
|
1625
1891
|
flash();
|
|
1626
|
-
} else if (sub === "
|
|
1892
|
+
} else if (sub === "debug") {
|
|
1893
|
+
const arg = rest.trim().toLowerCase();
|
|
1894
|
+
if (arg === "on" || arg === "enable" || arg === "true") {
|
|
1895
|
+
saveDebugState({ debug: true });
|
|
1896
|
+
ctx.ui.notify("🔍 Debug ON — verbose trace enabled (level=debug). Logs now include request IDs, thinking sniffing, and payload normalize details.", "info");
|
|
1897
|
+
} else if (arg === "off" || arg === "disable" || arg === "false") {
|
|
1898
|
+
saveDebugState({ debug: false });
|
|
1899
|
+
ctx.ui.notify("🔇 Debug OFF — level restored to info. File: " + DEBUG_STATE_FILE, "info");
|
|
1900
|
+
} else if (arg.startsWith("level")) {
|
|
1901
|
+
const lvl = arg.split(/\s+/)[1] as LogLevel | undefined;
|
|
1902
|
+
if (lvl && lvl in LOG_LEVEL_ORDER) {
|
|
1903
|
+
saveDebugState({ debug: false, level: lvl });
|
|
1904
|
+
ctx.ui.notify(`Log level set to ${lvl} (persisted to ${DEBUG_STATE_FILE})`, "info");
|
|
1905
|
+
} else {
|
|
1906
|
+
ctx.ui.notify(`Unknown level: ${lvl} (use debug/info/warn/error)`, "warn");
|
|
1907
|
+
}
|
|
1908
|
+
} else {
|
|
1909
|
+
const st = loadDebugState();
|
|
1910
|
+
const cur = st?.debug ? "debug (ON)" : (st?.level || process.env.FREEFLOW_LOG_LEVEL || "info");
|
|
1911
|
+
ctx.ui.notify(`Debug status: ${cur}\nFile: ${DEBUG_STATE_FILE}\nMinLevel: ${getMinLogLevel()} | isDebug=${isDebugEnabled()}\nUsage: /freeflow debug on|off | /freeflow debug level debug`, "info");
|
|
1912
|
+
}
|
|
1913
|
+
} else if (sub === "logs" || sub === "log" || sub === "trace") {
|
|
1627
1914
|
try {
|
|
1628
|
-
|
|
1629
|
-
|
|
1915
|
+
const rawRest = rest.trim();
|
|
1916
|
+
let filterLevel: LogLevel | null = null;
|
|
1917
|
+
let filterReqId: string | null = null;
|
|
1918
|
+
let count = 25;
|
|
1919
|
+
// trace mode: sub === trace or rest starts with trace/req
|
|
1920
|
+
if (sub === "trace" && rawRest) {
|
|
1921
|
+
filterReqId = rawRest.split(/\s+/)[0];
|
|
1922
|
+
} else if (rawRest) {
|
|
1923
|
+
const tokens = rawRest.split(/\s+/);
|
|
1924
|
+
for (const t of tokens) {
|
|
1925
|
+
const lower = t.toLowerCase();
|
|
1926
|
+
if (lower in LOG_LEVEL_ORDER) filterLevel = lower as LogLevel;
|
|
1927
|
+
else if (/^\d+$/.test(t)) count = Math.min(200, Math.max(5, parseInt(t, 10)));
|
|
1928
|
+
else if (/^[a-f0-9]{6,8}$/i.test(t) || t.startsWith("req=")) filterReqId = t.replace(/^req=/, "");
|
|
1929
|
+
else if (lower === "trace" || lower === "req") continue;
|
|
1930
|
+
else filterReqId = t;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
const files: string[] = [LOG_FILE, `${LOG_FILE}.1`, `${LOG_FILE}.2`].filter((f) => fs.existsSync(f));
|
|
1934
|
+
if (files.length === 0) {
|
|
1935
|
+
ctx.ui.notify("Log file is empty (no logs yet)", "info");
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
let allLines: string[] = [];
|
|
1939
|
+
for (const f of files) {
|
|
1940
|
+
try {
|
|
1941
|
+
const c = fs.readFileSync(f, "utf8");
|
|
1942
|
+
const ls = c.trim().split("\n").filter(Boolean);
|
|
1943
|
+
allLines = ls.concat(allLines);
|
|
1944
|
+
} catch {}
|
|
1945
|
+
}
|
|
1946
|
+
let filtered = allLines;
|
|
1947
|
+
if (filterLevel) {
|
|
1948
|
+
const want = `[${filterLevel.toUpperCase()}]`;
|
|
1949
|
+
filtered = filtered.filter((l) => l.includes(want));
|
|
1950
|
+
}
|
|
1951
|
+
if (filterReqId) {
|
|
1952
|
+
// match [reqId] bracket or req= prefix
|
|
1953
|
+
filtered = filtered.filter((l) => l.includes(filterReqId as string) || l.includes(`[${filterReqId}]`));
|
|
1954
|
+
}
|
|
1955
|
+
const lines = filtered.slice(-count);
|
|
1956
|
+
if (lines.length === 0) {
|
|
1957
|
+
ctx.ui.notify(`No logs matched (level=${filterLevel || "any"} reqId=${filterReqId || "any"} count=${count})`, "warning");
|
|
1630
1958
|
return;
|
|
1631
1959
|
}
|
|
1632
|
-
const
|
|
1633
|
-
|
|
1634
|
-
ctx.ui.notify(`pi-freeflow logs (last 25 lines from ${LOG_FILE}):\n\n${lines.join("\n")}`, "info");
|
|
1960
|
+
const header = `pi-freeflow logs (last ${lines.length}/${filtered.length} matched, total ${allLines.length} lines, file: ${LOG_FILE}${filterLevel ? ` level=${filterLevel}` : ""}${filterReqId ? ` req=${filterReqId}` : ""}):`;
|
|
1961
|
+
ctx.ui.notify(`${header}\n\n${lines.join("\n")}`, "info");
|
|
1635
1962
|
} catch (e) {
|
|
1636
1963
|
ctx.ui.notify(`Could not read log file: ${(e as Error).message}`, "error");
|
|
1637
1964
|
}
|