pi-freeflow 1.26.1 → 1.28.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/CHANGELOG.md +28 -0
- package/README.md +6 -6
- package/package.json +1 -1
- package/src/catalog.ts +10 -5
- package/src/cline-accounts.ts +962 -891
- package/src/commands.ts +1910 -1806
- package/src/deploy.ts +8 -5
- package/src/index.ts +1 -1
- package/src/models.ts +36 -23
- package/src/probe.ts +33 -1
- package/src/proxy.ts +25 -2
- package/src/relay-state.ts +1140 -1133
- package/src/relay.ts +422 -399
package/src/deploy.ts
CHANGED
|
@@ -16,17 +16,20 @@ import { randomBytes } from "node:crypto";
|
|
|
16
16
|
* whitelist, SSRF guard, path resolver, header denylist, auth gate and
|
|
17
17
|
* streaming forward live exactly once.
|
|
18
18
|
*
|
|
19
|
+
* The core ships inside String.raw, so backslashes below reach the deployed
|
|
20
|
+
* file verbatim — write them singly, never double them for the outer literal.
|
|
21
|
+
*
|
|
19
22
|
* @param relayAuth Per-deployment shared secret. Empty string disables the
|
|
20
23
|
* auth gate (legacy/manual deploys); built-in deploys always embed one.
|
|
21
24
|
*/
|
|
22
25
|
function buildRelayWorkerCore(relayAuth: string): string {
|
|
23
|
-
return `// Only the 2 upstreams pi-freeflow talks to. Anything else = open proxy abuse.
|
|
26
|
+
return String.raw`// Only the 2 upstreams pi-freeflow talks to. Anything else = open proxy abuse.
|
|
24
27
|
const ALLOWED_TARGETS = ["https://opencode.ai", "https://api.kilo.ai"];
|
|
25
28
|
const RELAY_AUTH = ${JSON.stringify(relayAuth)};
|
|
26
29
|
const resolveRelayTarget = function(target, relayPath) {
|
|
27
30
|
let targetUrl;
|
|
28
31
|
try { targetUrl = new URL(target); } catch { return { ok: false, status: 400, reason: "invalid x-relay-target" }; }
|
|
29
|
-
if (typeof relayPath !== "string" || relayPath.indexOf("@") !== -1 || relayPath.indexOf("
|
|
32
|
+
if (typeof relayPath !== "string" || relayPath.indexOf("@") !== -1 || relayPath.indexOf("\\") !== -1 || relayPath.charAt(0) !== "/") {
|
|
30
33
|
return { ok: false, status: 403, reason: "forbidden x-relay-path" };
|
|
31
34
|
}
|
|
32
35
|
let finalUrl;
|
|
@@ -38,7 +41,7 @@ const resolveRelayTarget = function(target, relayPath) {
|
|
|
38
41
|
};
|
|
39
42
|
const isPrivateHostname = function(h) {
|
|
40
43
|
if (!h) return true
|
|
41
|
-
let host = String(h).trim().toLowerCase().replace(
|
|
44
|
+
let host = String(h).trim().toLowerCase().replace(/^\[|\]$/g, "")
|
|
42
45
|
if (host.length > 1 && host.endsWith(".")) host = host.slice(0, -1)
|
|
43
46
|
if (!host) return true
|
|
44
47
|
// Numeric IP literals outside dotted-decimal (decimal 2130706433, short 127.1,
|
|
@@ -65,7 +68,7 @@ const isPrivateHostname = function(h) {
|
|
|
65
68
|
}
|
|
66
69
|
if (host === "localhost" || host === "0.0.0.0" || host === "127.0.0.1" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) return true
|
|
67
70
|
if (host.startsWith("::")) return true
|
|
68
|
-
const v4 = host.match(/^(
|
|
71
|
+
const v4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)
|
|
69
72
|
if (v4) {
|
|
70
73
|
const a = Number(v4[1])
|
|
71
74
|
const b = Number(v4[2])
|
|
@@ -109,7 +112,7 @@ async function relayHandler(req) {
|
|
|
109
112
|
if (targetUrl.protocol !== "http:" && targetUrl.protocol !== "https:") return new Response(JSON.stringify({ error: "forbidden x-relay-target protocol" }), { status: 403, headers: { "content-type": "application/json" } });
|
|
110
113
|
if (targetUrl.username || targetUrl.password) return new Response(JSON.stringify({ error: "forbidden x-relay-target (embedded credentials)" }), { status: 403, headers: { "content-type": "application/json" } });
|
|
111
114
|
if (isPrivateHostname(targetUrl.hostname)) return new Response(JSON.stringify({ error: "forbidden x-relay-target (private/loopback host)" }), { status: 403, headers: { "content-type": "application/json" } });
|
|
112
|
-
const cleanTarget = target.replace(
|
|
115
|
+
const cleanTarget = target.replace(/\/$/, "");
|
|
113
116
|
if (!ALLOWED_TARGETS.includes(cleanTarget)) return new Response(JSON.stringify({ error: "Forbidden target" }), { status: 403, headers: { "content-type": "application/json" } });
|
|
114
117
|
const relayPath = req.headers.get("x-relay-path") || "/";
|
|
115
118
|
const resolved = resolveRelayTarget(target, relayPath);
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* pi-freeflow — Modular, high-resiliency LLM extension for Pi & Oh My Pi (OMP)
|
|
3
3
|
*
|
|
4
|
-
* Provides access to 33 free models (
|
|
4
|
+
* Provides access to 33 free models (10 OpenCode Zen + 18 KiloCode Gateway + 5 Cline) with:
|
|
5
5
|
* - Single-port daemon reuse on 28180 across concurrent subagents
|
|
6
6
|
* - Multi-cloud rolling egress relays (Vercel Edge, Cloudflare, Deno)
|
|
7
7
|
* - 0ms instant startup with verified static catalog and background live health checks
|
package/src/models.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Static model definitions and upstream routing catalogs for pi-freeflow
|
|
3
3
|
*
|
|
4
|
-
* Defines the 33 verified free models:
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
4
|
+
* Defines the 33 verified free models (live-verified 2026-09-28):
|
|
5
|
+
* - 10 OpenCode Zen models (2 Responses API + 8 Chat Completions)
|
|
6
|
+
* - 18 KiloCode Keyless Gateway models (OpenRouter format)
|
|
7
7
|
* - 5 Cline direct-only models (per-user pool)
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -169,6 +169,23 @@ export const OPENCODE_MODELS: ModelDef[] = [
|
|
|
169
169
|
max: "max",
|
|
170
170
|
},
|
|
171
171
|
},
|
|
172
|
+
{
|
|
173
|
+
id: "longcat-2.5-preview-free",
|
|
174
|
+
name: "LongCat 2.5 Preview (1M) [OpenCode]",
|
|
175
|
+
reasoning: true,
|
|
176
|
+
contextWindow: 1_000_000,
|
|
177
|
+
maxTokens: 131_072,
|
|
178
|
+
input: ["text", "image"],
|
|
179
|
+
thinkingLevelMap: {
|
|
180
|
+
off: null,
|
|
181
|
+
minimal: "minimal",
|
|
182
|
+
low: "low",
|
|
183
|
+
medium: "medium",
|
|
184
|
+
high: "high",
|
|
185
|
+
xhigh: "xhigh",
|
|
186
|
+
max: "max",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
172
189
|
];
|
|
173
190
|
|
|
174
191
|
/**
|
|
@@ -321,21 +338,26 @@ export const KILO_MODELS: ModelDef[] = [
|
|
|
321
338
|
thinkingLevelMap: KILO_REASONING_MAP,
|
|
322
339
|
},
|
|
323
340
|
{
|
|
324
|
-
|
|
325
|
-
|
|
341
|
+
// Resurrected 2026-09-28: back on the live Kilo free list
|
|
342
|
+
// (isFree:true, 0/0 pricing, no expiry) + keyless chat 200.
|
|
343
|
+
id: "stepfun/step-3.7-flash:free",
|
|
344
|
+
name: "Step 3.7 Flash [Kilo]",
|
|
326
345
|
reasoning: true,
|
|
327
346
|
contextWindow: 262_144,
|
|
328
|
-
maxTokens:
|
|
347
|
+
maxTokens: 262_144,
|
|
329
348
|
input: ["text", "image"],
|
|
330
349
|
thinkingFormat: "openrouter",
|
|
331
350
|
thinkingLevelMap: KILO_REASONING_MAP,
|
|
332
351
|
},
|
|
333
352
|
{
|
|
334
|
-
|
|
335
|
-
|
|
353
|
+
// Added 2026-09-28: live on the Kilo free list (isFree:true, 0/0
|
|
354
|
+
// pricing, 1M context, text+image in) + keyless chat 200. Passes
|
|
355
|
+
// isFreeCatalogId via MODEL_MAP (no :free suffix by design).
|
|
356
|
+
id: "stealth/space-bunny-alpha",
|
|
357
|
+
name: "Space Bunny Alpha (1M) [Kilo]",
|
|
336
358
|
reasoning: true,
|
|
337
|
-
contextWindow:
|
|
338
|
-
maxTokens:
|
|
359
|
+
contextWindow: 1_000_000,
|
|
360
|
+
maxTokens: 524_288,
|
|
339
361
|
input: ["text", "image"],
|
|
340
362
|
thinkingFormat: "openrouter",
|
|
341
363
|
thinkingLevelMap: KILO_REASONING_MAP,
|
|
@@ -370,16 +392,6 @@ export const KILO_MODELS: ModelDef[] = [
|
|
|
370
392
|
thinkingFormat: "openrouter",
|
|
371
393
|
thinkingLevelMap: KILO_REASONING_MAP,
|
|
372
394
|
},
|
|
373
|
-
{
|
|
374
|
-
id: "z-ai/glm-5.2:free",
|
|
375
|
-
name: "GLM 5.2 [Kilo]",
|
|
376
|
-
reasoning: true,
|
|
377
|
-
contextWindow: 32_768,
|
|
378
|
-
maxTokens: 29_491,
|
|
379
|
-
input: ["text"],
|
|
380
|
-
thinkingFormat: "openrouter",
|
|
381
|
-
thinkingLevelMap: KILO_REASONING_MAP,
|
|
382
|
-
},
|
|
383
395
|
];
|
|
384
396
|
|
|
385
397
|
/**
|
|
@@ -498,13 +510,14 @@ export const MODEL_ALIASES: Record<string, string> = {
|
|
|
498
510
|
"content-safety": "nvidia/nemotron-3.5-content-safety:free",
|
|
499
511
|
"ling-3.0-flash-fin": "inclusionai/ling-3.0-flash-fin:free",
|
|
500
512
|
"ling-3.0-flash-sante": "inclusionai/ling-3.0-flash-sante:free",
|
|
501
|
-
"
|
|
502
|
-
"
|
|
513
|
+
"step-3.7-flash": "stepfun/step-3.7-flash:free",
|
|
514
|
+
"space-bunny-alpha": "stealth/space-bunny-alpha",
|
|
503
515
|
"inkling-small": "thinkingmachines/inkling-small:free",
|
|
504
516
|
"mimo-v2.6-flash": "mimo-v2.6-flash-free",
|
|
505
517
|
"space-bunny": "space-bunny-free",
|
|
518
|
+
"longcat-2.5-preview": "longcat-2.5-preview-free",
|
|
519
|
+
"longcat": "longcat-2.5-preview-free",
|
|
506
520
|
"qwen3.8-27b": "qwen/qwen3.8-27b:free",
|
|
507
|
-
"glm-5.2": "z-ai/glm-5.2:free",
|
|
508
521
|
// provider-prefixed short aliases (slash-normalized)
|
|
509
522
|
"laguna-s-2.1:free": "poolside/laguna-s-2.1:free",
|
|
510
523
|
"laguna-xs-2.1:free": "poolside/laguna-xs-2.1:free",
|
package/src/probe.ts
CHANGED
|
@@ -24,6 +24,15 @@ export interface RelayProbeOptions {
|
|
|
24
24
|
model?: string;
|
|
25
25
|
/** Timeout in milliseconds (defaults to 5000) */
|
|
26
26
|
timeoutMs?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Number of retry attempts when the probe returns !ok or throws.
|
|
29
|
+
* Defaults to 0 (single attempt). Useful post-deploy for edge DNS propagation.
|
|
30
|
+
*/
|
|
31
|
+
retries?: number;
|
|
32
|
+
/** Delay between retries in milliseconds (defaults to 2000). */
|
|
33
|
+
retryDelayMs?: number;
|
|
34
|
+
/** Optional progress callback before each retry */
|
|
35
|
+
onRetry?: (attempt: number, maxRetries: number, lastResult: RelayProbeResult) => void;
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
/**
|
|
@@ -41,7 +50,7 @@ export interface RelayProbeOptions {
|
|
|
41
50
|
* @param auth Optional per-relay shared secret; sent as x-relay-auth so
|
|
42
51
|
* deployed relays (which enforce it) answer the probe instead of 401.
|
|
43
52
|
*/
|
|
44
|
-
|
|
53
|
+
async function probeRelayOnce(
|
|
45
54
|
url: string,
|
|
46
55
|
auth?: string,
|
|
47
56
|
opts?: RelayProbeOptions,
|
|
@@ -119,3 +128,26 @@ export async function probeRelay(
|
|
|
119
128
|
};
|
|
120
129
|
}
|
|
121
130
|
}
|
|
131
|
+
|
|
132
|
+
export async function probeRelay(
|
|
133
|
+
url: string,
|
|
134
|
+
auth?: string,
|
|
135
|
+
opts?: RelayProbeOptions,
|
|
136
|
+
): Promise<RelayProbeResult> {
|
|
137
|
+
const retries = Math.max(0, opts?.retries ?? 0);
|
|
138
|
+
const retryDelay = Math.max(10, opts?.retryDelayMs ?? 2000);
|
|
139
|
+
|
|
140
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
141
|
+
if (attempt > 0) {
|
|
142
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
143
|
+
setTimeout(resolve, retryDelay);
|
|
144
|
+
await promise;
|
|
145
|
+
}
|
|
146
|
+
const result = await probeRelayOnce(url, auth, opts);
|
|
147
|
+
if (result.ok || attempt === retries) {
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
opts?.onRetry?.(attempt + 1, retries, result);
|
|
151
|
+
}
|
|
152
|
+
return { ok: false, status: 0, latencyMs: 0, error: "Probe failed" };
|
|
153
|
+
}
|
package/src/proxy.ts
CHANGED
|
@@ -126,6 +126,27 @@ function withRateLimitHint(status: number, data: string): string {
|
|
|
126
126
|
} catch { }
|
|
127
127
|
return data;
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Exhausted-disabled relay verdict from the relay layer (HTTP 503 JSON with
|
|
131
|
+
* `error.code === "relay_disabled"`): every candidate rolled as disabled and
|
|
132
|
+
* the direct fallback is unavailable, so the host must retry/fail fast
|
|
133
|
+
* instead of parking on a provider wait. Passes through to the host
|
|
134
|
+
* unchanged — hint rewrites must never touch its code or guidance.
|
|
135
|
+
*/
|
|
136
|
+
export function isRelayDisabledError(status: number, data: string): boolean {
|
|
137
|
+
if (status !== 503) return false;
|
|
138
|
+
try {
|
|
139
|
+
const parsed: unknown = JSON.parse(data);
|
|
140
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return false;
|
|
141
|
+
if (!("error" in parsed)) return false;
|
|
142
|
+
const err: unknown = parsed.error;
|
|
143
|
+
if (typeof err !== "object" || err === null || Array.isArray(err)) return false;
|
|
144
|
+
if (!("code" in err)) return false;
|
|
145
|
+
return err.code === "relay_disabled";
|
|
146
|
+
} catch {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
129
150
|
/** Cline marker errors: each status carries account-actionable guidance. */
|
|
130
151
|
const CLINE_RATE_LIMIT_HINT =
|
|
131
152
|
"Cline free-use limit reached for this login. Wait for the reset or switch models.";
|
|
@@ -1300,7 +1321,7 @@ export function startProxy(
|
|
|
1300
1321
|
);
|
|
1301
1322
|
} else {
|
|
1302
1323
|
if (!response.ok) {
|
|
1303
|
-
log("warn", `upstream ${response.status} for model ${String((parsedBody as Record<string, unknown> | null)?.model ?? "?")} via relay`, { status: response.status, model: (parsedBody as Record<string, unknown> | null)?.model, path: req.url }, reqId);
|
|
1324
|
+
log("warn", `upstream ${response.status} for model ${String((parsedBody as Record<string, unknown> | null)?.model ?? "?")} via relay ${relayState.url || "pool"}`, { status: response.status, model: (parsedBody as Record<string, unknown> | null)?.model, path: req.url, relay: relayState.url }, reqId);
|
|
1304
1325
|
}
|
|
1305
1326
|
let rawText = await response.text();
|
|
1306
1327
|
let ct =
|
|
@@ -1310,7 +1331,9 @@ export function startProxy(
|
|
|
1310
1331
|
rawText = convertSseToJson(rawText, target.pathname, callerHadTools, callerCaseRestore, callerFindGlob, callerInjected);
|
|
1311
1332
|
ct = "application/json";
|
|
1312
1333
|
}
|
|
1313
|
-
const data =
|
|
1334
|
+
const data = isRelayDisabledError(response.status, rawText)
|
|
1335
|
+
? rawText
|
|
1336
|
+
: withFreeTierHint(response.status, withRateLimitHint(response.status, rawText));
|
|
1314
1337
|
res.writeHead(response.status, { "content-type": ct });
|
|
1315
1338
|
res.end(data);
|
|
1316
1339
|
}
|