pi-freeflow 1.4.3 → 1.4.4

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-freeflow 🌊
2
2
 
3
- > **23 free models. Up to 1M context. Zero API keys. Infinite scale via your own relay pool.**
3
+ > **21 free models. Up to 1M context. Zero API keys. Infinite scale via your own relay pool.**
4
4
 
5
5
  Thin by design: model list + dumb relay + log. Host `pi-ai` owns thinking, normalization & provider magic. We just make it free, fast, and unbreakable.
6
6
 
@@ -18,7 +18,7 @@ Join devs bypassing rate limits with their own relay pools. BYO, add as many as
18
18
 
19
19
  | Feature | Description | Value | Cost |
20
20
  | :--- | :--- | :--- | :--- |
21
- | **23 Curated Free Models** | 9 OpenCode Zen + 14 KiloCode Gateway models, up to 1M context & 512K output | Ceiling Unlocked | **$0** |
21
+ | **21 Curated Free Models** | 7 OpenCode Zen + 14 KiloCode Gateway models, up to 1M context & 512K output | Ceiling Unlocked | **$0** |
22
22
  | **BYO Relay Pool** | Round-robin load balancing across your Cloudflare Workers & Vercel Edges | Zero Rate Limits | **$0** (your free tiers) |
23
23
  | **Adaptive Health & Error Detection** | Auto-cooldown on 429 rate limits, 504 timeouts, and socket drops | 0ms Wasted Latency | **$0** |
24
24
  | **Stream Truncation Resilience** | Stateful SSE terminal tracking (`response.failed` / `response.incomplete` injection) | Zero Host Crashes | **$0** |
@@ -32,21 +32,19 @@ Philosophy: **Thin by design.** We only ship model list + relay proxy + log. Hos
32
32
 
33
33
  ---
34
34
 
35
- ### 23 Curated Models, One Command
35
+ ### 21 Curated Models, One Command
36
36
 
37
37
  ```bash
38
38
  /model → freeflow → pick
39
39
  ```
40
40
 
41
- #### OpenCode Zen (9 Models), Responses & Chat API
41
+ #### OpenCode Zen (7 Models), Responses & Chat API
42
42
  Optimized for deep reasoning, long-horizon coding & autonomous agentic workflows.
43
43
 
44
44
  | Model ID | Creator / Lab | Context | Max Output | Thinking | Vision |
45
45
  | :--- | :--- | :--- | :--- | :--- | :--- |
46
46
  | `muse-spark-1.2-contributor-free` | Meta Superintelligence Labs | **1M** (1.048.576) | **131K** (131.072) | `minimal / low / medium / high / xhigh / max` | ✅ |
47
- | `x-preview-f-free` | Ox Alpha | **1M** (1.048.576) | **131K** (131.072) | `low / high / max` | ✅ |
48
47
  | `mimo-v2.5-free` | Xiaomi MiMo | **1M** (1.048.576) | **131K** (131.072) | `low / medium / high` | ✅ |
49
- | `deepseek-v4-flash-free` | DeepSeek | **1M** (1.000.000) | **384K** (384.000) | `low / high / max` | ❌ |
50
48
  | `laguna-s-2.1-free` | Poolside | **1M** (1.048.576) | **131K** (131.072) | `low / high / max` | ❌ |
51
49
  | `nemotron-3.5-lightning-free` | NVIDIA | **1M** (1.000.000) | **262K** (262.144) | `low / high / max` | ❌ |
52
50
  | `nemotron-3-ultra-free` | NVIDIA | **1M** (1.000.000) | **128K** (128.000) | `low / high / max` | ❌ |
@@ -285,7 +283,7 @@ pnpm smoke # verifies extensions/index.ts loads without crashing
285
283
  ```
286
284
  src/
287
285
  ├── index.ts # extension entry, lifecycle hooks
288
- ├── models.ts # 23-model catalog definitions
286
+ ├── models.ts # 21-model catalog definitions
289
287
  ├── catalog.ts # model catalog cache (24h disk)
290
288
  ├── proxy.ts # local proxy server (127.0.0.1:18080)
291
289
  ├── relay.ts # relay selection & round-robin
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-freeflow",
3
3
  "type": "module",
4
- "version": "1.4.3",
4
+ "version": "1.4.4",
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/catalog.ts CHANGED
@@ -31,9 +31,14 @@ import type {
31
31
  Upstream,
32
32
  } from "./types.ts";
33
33
 
34
+ /**
35
+ * Pruned model IDs that must never re-enter the catalog via disk cache or upstream merge.
36
+ */
37
+ const DEAD_MODEL_IDS = new Set<string>(["deepseek-v4-flash-free", "x-preview-f-free"]);
38
+
34
39
  /**
35
40
  * In-memory cache of currently active/available free models.
36
- * Initialized with all 23 verified models for 0ms instant availability.
41
+ * Initialized with all 21 verified models for 0ms instant availability.
37
42
  */
38
43
  let aliveCatalog: RegisteredModel[] = ALL_MODELS.map((m) => ({
39
44
  ...m,
@@ -58,14 +63,17 @@ export function setAliveCatalog(catalog: RegisteredModel[]): void {
58
63
  * Fresh entries win on id collision; unknown fresh ids are appended after the base.
59
64
  * Guards the background refresh against a partial upstream cache removing
60
65
  * verified static models from provider registration.
66
+ * Filters pruned dead IDs so they never re-enter via fresh upstream data.
61
67
  */
62
68
  export function mergeCatalog(
63
69
  base: RegisteredModel[],
64
70
  fresh: RegisteredModel[],
65
71
  ): RegisteredModel[] {
72
+ const filteredFresh = fresh.filter((m) => !DEAD_MODEL_IDS.has(m.id));
66
73
  const byId = new Map(base.map((m) => [m.id, m]));
67
- for (const m of fresh) byId.set(m.id, m);
68
- return [...byId.values()];
74
+ for (const m of filteredFresh) byId.set(m.id, m);
75
+ // Ensure no dead IDs survive even if base was stale
76
+ return [...byId.values()].filter((m) => !DEAD_MODEL_IDS.has(m.id));
69
77
  }
70
78
 
71
79
  /**
@@ -122,7 +130,6 @@ export function enrichModelDef(raw: RawModelItem, source: Upstream): RegisteredM
122
130
  idLower.includes("r1") ||
123
131
  idLower.includes("o1") ||
124
132
  idLower.includes("think") ||
125
- idLower.includes("alpha") ||
126
133
  idLower.includes("spark");
127
134
 
128
135
  let contextWindow =
@@ -162,6 +169,7 @@ export function enrichModelDef(raw: RawModelItem, source: Upstream): RegisteredM
162
169
 
163
170
  /**
164
171
  * Read cached catalog data from disk if valid and unexpired.
172
+ * Filters out pruned dead IDs so stale disk entries never repopulate the catalog.
165
173
  */
166
174
  export function readCatalogCache(): CatalogCacheData | null {
167
175
  try {
@@ -170,6 +178,9 @@ export function readCatalogCache(): CatalogCacheData | null {
170
178
  }
171
179
  const raw = fs.readFileSync(CATALOG_CACHE_FILE, "utf8");
172
180
  const data = JSON.parse(raw) as CatalogCacheData;
181
+ if (Array.isArray(data.models)) {
182
+ data.models = data.models.filter((m) => !DEAD_MODEL_IDS.has(m.id));
183
+ }
173
184
  if (Date.now() - data.timestamp < CATALOG_CACHE_TTL_MS) {
174
185
  return data;
175
186
  }
@@ -203,20 +214,37 @@ export function writeCatalogCache(data: CatalogCacheData): void {
203
214
  export async function refreshCatalog(force = false): Promise<RegisteredModel[]> {
204
215
  // Thin provider: no live fetch — subagents must not hit upstream directly
205
216
  // (proxy-only). Host Pi/OMP owns dynamic discovery via fetchDynamicModels (24h).
206
- // We only serve disk cache if fresh, otherwise static 23-model aliveCatalog.
217
+ // We only serve disk cache if fresh, otherwise static 21-model aliveCatalog.
207
218
  const disk = readCatalogCache();
208
219
  if (disk && Array.isArray(disk.models) && disk.models.length > 0) {
209
220
  const age = Date.now() - (disk.timestamp ?? 0);
210
221
  if (!force && age < CATALOG_CACHE_TTL_MS) {
211
- aliveCatalog = disk.models;
222
+ aliveCatalog = disk.models.filter((m) => !DEAD_MODEL_IDS.has(m.id));
212
223
  return aliveCatalog;
213
224
  }
214
- // Stale cache still better than empty — return it without network
215
- if (disk.models.length >= 23) {
216
- aliveCatalog = disk.models;
225
+ // Stale cache still better than empty — return it without network (filtered)
226
+ const filtered = disk.models.filter((m) => !DEAD_MODEL_IDS.has(m.id));
227
+ if (filtered.length >= 21) {
228
+ aliveCatalog = filtered;
217
229
  return aliveCatalog;
218
230
  }
219
231
  }
220
- // No valid cache — return in-memory static 23 (host will refresh if needed)
232
+ // No valid fresh cache — try stale disk cache directly (readCatalogCache returns null when expired)
233
+ try {
234
+ if (fs.existsSync(CATALOG_CACHE_FILE)) {
235
+ const raw = fs.readFileSync(CATALOG_CACHE_FILE, "utf8");
236
+ const stale = JSON.parse(raw) as CatalogCacheData;
237
+ if (Array.isArray(stale.models) && stale.models.length > 0) {
238
+ const filtered = stale.models.filter((m) => !DEAD_MODEL_IDS.has(m.id));
239
+ if (filtered.length >= 21) {
240
+ aliveCatalog = filtered;
241
+ return aliveCatalog;
242
+ }
243
+ }
244
+ }
245
+ } catch (err) {
246
+ logDebug("Failed reading stale catalog cache", { error: String(err) });
247
+ }
248
+ // No valid cache — return in-memory static 21 (host will refresh if needed)
221
249
  return aliveCatalog;
222
250
  }
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 23 free models (9 OpenCode Zen + 14 KiloCode Gateway) with:
4
+ * Provides access to 21 free models (7 OpenCode Zen + 14 KiloCode Gateway) with:
5
5
  * - Single-port daemon reuse on 18080 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,8 +1,8 @@
1
1
  /**
2
2
  * Static model definitions and upstream routing catalogs for pi-freeflow
3
3
  *
4
- * Defines the 23 verified free models:
5
- * - 9 OpenCode Zen models (1 Responses API + 8 Chat Completions)
4
+ * Defines the 21 verified free models:
5
+ * - 7 OpenCode Zen models (1 Responses API + 6 Chat Completions)
6
6
  * - 14 KiloCode Keyless Gateway models (10 OpenRouter format + 4 Standard format)
7
7
  */
8
8
 
@@ -13,40 +13,6 @@ import type { ModelDef, Upstream } from "./types.ts";
13
13
  * Endpoint: https://opencode.ai/zen/v1
14
14
  */
15
15
  export const OPENCODE_MODELS: ModelDef[] = [
16
- {
17
- id: "deepseek-v4-flash-free",
18
- name: "DeepSeek V4 Flash (1M)",
19
- reasoning: true,
20
- contextWindow: 1_000_000,
21
- maxTokens: 384_000,
22
- input: ["text"],
23
- thinkingLevelMap: {
24
- off: null,
25
- minimal: "low",
26
- low: "low",
27
- medium: "high",
28
- high: "high",
29
- xhigh: "max",
30
- max: "max",
31
- },
32
- },
33
- {
34
- id: "x-preview-f-free",
35
- name: "Ox Alpha (1M)",
36
- reasoning: true,
37
- contextWindow: 1_048_576,
38
- maxTokens: 131_072,
39
- input: ["text", "image"],
40
- thinkingLevelMap: {
41
- off: null,
42
- minimal: "low",
43
- low: "low",
44
- medium: "high",
45
- high: "high",
46
- xhigh: "max",
47
- max: "max",
48
- },
49
- },
50
16
  {
51
17
  id: "muse-spark-1.2-contributor-free",
52
18
  name: "Muse Spark 1.2 (1M)",
@@ -305,37 +271,27 @@ export const KILO_MODELS: ModelDef[] = [
305
271
 
306
272
  /**
307
273
  * Model ID Aliases — maps user-friendly / slash-free CLI IDs to canonical upstream model IDs.
274
+ * Single clean alias per model (no :free duplicates). Wrong cross-lab aliases removed:
275
+ * - Muse Spark 1.2 = Meta Superintelligence Labs (not Anthropic Claude) → removed claude-sonnet aliases
276
+ * - Laguna S 2.1 = Poolside (not MiniMax) → removed minimax-m2.1 alias
277
+ * - Hy3 = Tencent Hunyuan (not Alibaba Qwen) → removed qwen3-coder alias
308
278
  */
309
279
  export const MODEL_ALIASES: Record<string, string> = {
310
- // Kilo Gateway slash-free & colon-free CLI aliases
280
+ // Kilo Gateway slash-free & colon-free clean aliases (one per model)
311
281
  "dots-3-note-preview": "dots-studio/dots-3-note-preview:free",
312
- "dots-3-note-preview:free": "dots-studio/dots-3-note-preview:free",
313
282
  "step-3.7-flash": "stepfun/step-3.7-flash:free",
314
- "step-3.7-flash:free": "stepfun/step-3.7-flash:free",
315
283
  "nemotron-3-nano-omni": "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
316
- "nemotron-3-nano-omni:free": "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
317
284
  "nemotron-3-ultra-550b": "nvidia/nemotron-3-ultra-550b-a55b:free",
318
- "nemotron-3-ultra-550b:free": "nvidia/nemotron-3-ultra-550b-a55b:free",
319
285
  "nemotron-3-super": "nvidia/nemotron-3-super-120b-a12b:free",
320
- "nemotron-3-super:free": "nvidia/nemotron-3-super-120b-a12b:free",
321
- "hy3:free": "tencent/hy3:free",
322
286
  "north-mini-code": "cohere/north-mini-code:free",
323
- "north-mini-code:free": "cohere/north-mini-code:free",
324
- "laguna-s-2.1:free": "poolside/laguna-s-2.1:free",
325
- "laguna-xs-2.1:free": "poolside/laguna-xs-2.1:free",
326
287
  "lfm-2.5": "liquid/lfm-2.5-2.6b:free",
327
- "lfm-2.5:free": "liquid/lfm-2.5-2.6b:free",
328
288
  "content-safety": "nvidia/nemotron-3.5-content-safety:free",
329
- "content-safety:free": "nvidia/nemotron-3.5-content-safety:free",
289
+ // provider-prefixed short aliases (slash-normalized)
290
+ "hy3:free": "tencent/hy3:free",
291
+ "laguna-s-2.1:free": "poolside/laguna-s-2.1:free",
292
+ "laguna-xs-2.1:free": "poolside/laguna-xs-2.1:free",
330
293
  "kilo-auto": "kilo-auto/free",
331
294
  "openrouter": "openrouter/free",
332
-
333
- // OpenCode Zen aliases
334
- "claude-sonnet-4.5-free": "muse-spark-1.2-contributor-free",
335
- "claude-sonnet-4.5-contributor-free": "muse-spark-1.2-contributor-free",
336
- "grok-code-fast-1-preview-f-free": "x-preview-f-free",
337
- "minimax-m2.1-free": "laguna-s-2.1-free",
338
- "qwen3-coder-480b-free": "hy3-free",
339
295
  };
340
296
 
341
297
  /**
@@ -357,7 +313,7 @@ export const KILO_MODEL_IDS = new Set<string>([
357
313
  ]);
358
314
 
359
315
  /**
360
- * Combined list of all 23 static free models (canonical)
316
+ * Combined list of all 21 static free models (canonical)
361
317
  */
362
318
  export const ALL_MODELS: ModelDef[] = [...OPENCODE_MODELS, ...KILO_MODELS];
363
319
 
package/src/proxy.ts CHANGED
@@ -88,21 +88,11 @@ export function sanitizeHeaders(
88
88
  }
89
89
 
90
90
  /**
91
- * Clamps reasoning_effort for upstream models with strict non-standard enums
92
- * (e.g. OpenCode x-preview strictly requires 'low', 'high', or 'max' and rejects 'medium' with 400).
91
+ * Reasoning normalization is owned by host pi-ai; proxy passes through reasoning fields unchanged.
92
+ * Kept as no-op for compatibility no per-model clamping.
93
93
  */
94
- function sanitizeReasoningForModel(bodyObj: Record<string, unknown>): void {
95
- const model = String(bodyObj.model || "").toLowerCase();
96
- if (model.includes("x-preview")) {
97
- const effort = String(bodyObj.reasoning_effort || "").toLowerCase();
98
- if (effort === "medium") {
99
- bodyObj.reasoning_effort = "high";
100
- } else if (effort === "minimal") {
101
- bodyObj.reasoning_effort = "low";
102
- } else if (!effort || effort === "off" || effort === "none") {
103
- bodyObj.reasoning_effort = "low";
104
- }
105
- }
94
+ function sanitizeReasoningForModel(_bodyObj: Record<string, unknown>): void {
95
+ // no-op
106
96
  }
107
97
  /**
108
98
  * Probe whether an existing pi-freeflow proxy daemon is running and responsive on a given port.