pi-freeflow 1.35.0 → 1.36.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 CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.36.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bf358da: A new free model on the OpenCode Zen lane: Cat Jam
8
+
9
+ `cat-jam-free` joins the picker, bringing the catalog to 37 models (11 OpenCode, 15 Kilo, 2 Cline, 9 OVHcloud). No account, no key, no setup — it is keyless like every other Zen free model.
10
+
11
+ It thinks, and it has a 262,144-token window. That number was measured the hard way: the gateway happily accepts prompts four times that size and answers normally, but the model can no longer read them. A marker planted at the end of a 248,232-token prompt comes back; at 250,272 tokens it does not. So the advertised window is what the model can actually use, not merely what the service tolerates.
12
+
13
+ Two honest limits worth knowing before you pick it:
14
+
15
+ - **It is not available on the Responses API.** Requests sent that way are refused with a protocol error. It works on chat completions, which is what hosts use by default, but a host pinned specifically to Responses will not see it.
16
+ - **Thinking depth starts at "low".** The lowest setting is refused, so "minimal" is not offered; low, medium, high and extra-high all work.
17
+
18
+ The output budget is 131,072, the published figure for this model.
19
+
20
+ ### Patch Changes
21
+
22
+ - bf358da: Long sessions no longer stall on a tool call whose arguments arrived spliced
23
+
24
+ Some models open their next tool call by echoing the tail of the one they had just seen, and occasionally stop before the closing brace. The result is a single argument string carrying both faults at once: the previous call's leftover text in front, and the last brace missing. The whole call was then rejected, the turn failed, and the host retried — which resent the same broken payload, so a long session could get stuck repeating it.
25
+
26
+ Both faults are now repaired together, so the caller's own arguments arrive intact.
27
+
28
+ The repair stays deliberately narrow. It only fires when a leftover fragment is actually present in front of the object, which is the evidence that the model was mid-call rather than that the request itself was malformed, and only when what follows is a complete object merely missing its closing brace. Arguments cut off part-way through a value are still refused untouched, because completing those would silently shorten them — and these arguments carry shell commands.
29
+
30
+ Tool calls that were already well-formed are unaffected and pass through byte-identically.
31
+
3
32
  ## 1.35.0
4
33
 
5
34
  ### Minor Changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-freeflow
2
2
 
3
- > 36 free models with up to 1M context. No API keys to manage. Add your own relays to spread requests across more IPs.
3
+ > 37 free models with up to 1M context. No API keys to manage. Add your own relays to spread requests across more IPs.
4
4
 
5
5
  Thin by design: a model list, a relay proxy, and a log. The host (`pi-ai`) handles thinking, normalization, and provider behavior.
6
6
 
@@ -16,7 +16,7 @@ Thin by design: a model list, a relay proxy, and a log. The host (`pi-ai`) handl
16
16
 
17
17
  | Feature | What it does | Cost |
18
18
  | :--- | :--- | :--- |
19
- | **36 free models** | 10 from OpenCode Zen, 15 from KiloCode Gateway, 2 from Cline, 9 from OVHcloud AI Endpoints, context windows up to 1M. Full list below. | **$0** |
19
+ | **37 free models** | 11 from OpenCode Zen, 15 from KiloCode Gateway, 2 from Cline, 9 from OVHcloud AI Endpoints, context windows up to 1M. Full list below. | **$0** |
20
20
  | **Relay pool** | Route requests through your own Cloudflare Workers, Deno Deploy, and Vercel Edge relays. Traffic spreads across the pool, or stays on one sticky relay. A relay that rate-limits, times out, or drops the connection cools down while healthy ones take its traffic. | **$0** beyond your platforms' free tiers |
21
21
  | **Automatic fallback** | When every relay is cooling down, requests go direct to upstream instead of failing. | **$0** |
22
22
  | **Short model names** | Most models have a slash-free, colon-free alias, plus an optional `:effort` suffix for thinking depth. You type `freeflow/<name>`; the two Cline models have no alias and must be typed in full. | **$0** |
@@ -185,18 +185,19 @@ The same command set works identically in OMP and Pi:
185
185
 
186
186
  ---
187
187
 
188
- ### 36 models, one command
188
+ ### 37 models, one command
189
189
 
190
190
  ```bash
191
191
  /model → freeflow → pick
192
192
  ```
193
193
 
194
- #### OpenCode Zen (10 models), Responses and Chat API
194
+ #### OpenCode Zen (11 models), Responses and Chat API
195
195
 
196
196
  Good defaults for long coding sessions and agentic work.
197
197
 
198
198
  | Model ID | Creator / Lab | Context | Max Output | Thinking | Vision |
199
199
  | :--- | :--- | :--- | :--- | :--- | :--- |
200
+ | `cat-jam-free` | Cat Jam | **256K** (262.144) | **131K** (131.072) | `low … xhigh` | ❌ |
200
201
  | `muse-spark-1.2-contributor-free` | Meta Superintelligence Labs | **1M** (1.048.576) | **131K** (131.072) | `minimal … xhigh` | ✅ |
201
202
  | `muse-spark-1.3-contributor-free` | Meta Superintelligence Labs | **1M** (1.048.576) | **131K** (131.072) | `minimal … xhigh` | ✅ |
202
203
  | `mimo-v2.6-flash-free` | Xiaomi MiMo | **1M** (1.048.576) | **131K** (131.072) | `minimal … xhigh`\* | ✅ |
@@ -358,7 +359,7 @@ pnpm smoke # verifies extensions/index.ts loads without crashing
358
359
  ```
359
360
  src/
360
361
  ├── index.ts # extension entry, lifecycle hooks
361
- ├── models.ts # 36-model catalog definitions
362
+ ├── models.ts # 37-model catalog definitions
362
363
  ├── catalog.ts # model catalog cache (24h disk)
363
364
  ├── proxy.ts # local proxy server (127.0.0.1:28180)
364
365
  ├── client.ts # session-side daemon attach/spawn and heartbeat
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-freeflow",
3
3
  "type": "module",
4
- "version": "1.35.0",
4
+ "version": "1.36.0",
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/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 36 free models (10 OpenCode Zen + 15 KiloCode Gateway + 2 Cline + 9 OVH) with:
4
+ * Provides access to 37 free models (11 OpenCode Zen + 15 KiloCode Gateway + 2 Cline + 9 OVH) 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,8 +1,8 @@
1
1
  /**
2
2
  * Static model definitions and upstream routing catalogs for pi-freeflow
3
3
  *
4
- * Defines the 36 verified free models (live-verified 2026-10-13):
5
- * - 10 OpenCode Zen models (2 Responses API + 8 Chat Completions)
4
+ * Defines the 37 verified free models (live-verified 2026-10-13):
5
+ * - 11 OpenCode Zen models (2 Responses API + 9 Chat Completions)
6
6
  * - 15 KiloCode Keyless Gateway models (OpenRouter format)
7
7
  * - 2 Cline direct-only models (per-user pool)
8
8
  * - 9 OVHcloud AI Endpoints models (keyless, direct-only)
@@ -15,6 +15,27 @@ import type { ModelDef, ThinkingLevelMap, Upstream } from "./types.ts";
15
15
  * Endpoint: https://opencode.ai/zen/v1
16
16
  */
17
17
  export const OPENCODE_MODELS: ModelDef[] = [
18
+ {
19
+ id: "cat-jam-free",
20
+ name: "Cat Jam [OpenCode]",
21
+ reasoning: true,
22
+ // Context is 262_144, NOT the 1M an acceptance probe suggests: the gateway
23
+ // answers 200 to oversized prompts and silently truncates. A needle planted
24
+ // at the end of a 248,232-token prompt is recalled; at 250,272 it is not.
25
+ // Cross-checked against the published spec, which agrees.
26
+ contextWindow: 262_144,
27
+ maxTokens: 131_072,
28
+ input: ["text"],
29
+ thinkingLevelMap: {
30
+ off: null,
31
+ minimal: null,
32
+ low: "low",
33
+ medium: "medium",
34
+ high: "high",
35
+ xhigh: "xhigh",
36
+ max: null,
37
+ },
38
+ },
18
39
  {
19
40
  id: "muse-spark-1.2-contributor-free",
20
41
  name: "Muse Spark 1.2 [OpenCode]",
package/src/tool-args.ts CHANGED
@@ -357,8 +357,12 @@ export function buildArgRepairSchemas(tools: unknown[]): Map<string, Record<stri
357
357
  *
358
358
  * Both are one complete JSON object with junk in front of it. Only strings that
359
359
  * do not parse as-is are considered, and only a position that yields a complete
360
- * object is accepted, so a well-formed call never reaches this path and a
361
- * truncated one is still refused rather than completed.
360
+ * object is accepted, so a well-formed call never reaches this path.
361
+ *
362
+ * A second live shape (2026-10-13) carried BOTH faults at once — the previous
363
+ * call's tail in front AND the closing brace missing — so the scan also offers
364
+ * the candidate with its closers appended. That offer is made only at the first
365
+ * brace and only when a fragment precedes it; see appendClosers.
362
366
  *
363
367
  * Returns undefined when nothing salvageable is present.
364
368
  */
@@ -370,18 +374,67 @@ function parseArguments(argsJson: string): { value: Record<string, unknown>; sal
370
374
  } catch {
371
375
  // fall through to the salvage scan
372
376
  }
373
- for (let i = argsJson.indexOf("{"); i !== -1; i = argsJson.indexOf("{", i + 1)) {
374
- try {
375
- const value = JSON.parse(argsJson.slice(i));
376
- if (value !== null && typeof value === "object" && !Array.isArray(value)) {
377
- return { value: value as Record<string, unknown>, salvaged: true };
378
- }
379
- } catch {
380
- // not a start position; try the next brace
377
+ for (let i = argsJson.indexOf("{"); i !== -1; i = argsJson.indexOf("{", i + 1)) {
378
+ const candidate = argsJson.slice(i);
379
+ // Two shapes to try, in order: the candidate as written, then the candidate
380
+ // with its missing closers appended. The closer is only offered at the FIRST
381
+ // brace and only when something precedes it — that fragment is the evidence
382
+ // the model was mid-call and lost the tail. A bare `{"op":"init"` stays
383
+ // refused, and so does an inner `{"phase":…` inside an already-truncated
384
+ // object, which is why this is not offered at every brace.
385
+ const candidates = i === 0 ? [candidate] : [candidate, appendClosers(candidate)];
386
+ for (const attempt of candidates) {
387
+ if (attempt === undefined) continue;
388
+ try {
389
+ const value = JSON.parse(attempt);
390
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
391
+ return { value: value as Record<string, unknown>, salvaged: true };
381
392
  }
393
+ } catch {
394
+ // not a start position; try the next brace
395
+ }
382
396
  }
397
+ }
383
398
  return undefined;
384
399
  }
400
+
401
+ /**
402
+ * Append the closers an unterminated JSON object is missing, or undefined when
403
+ * that would mean guessing.
404
+ *
405
+ * Only ever called on a candidate that starts at a `{` preceded by a fragment
406
+ * of the previous call, and only when the candidate's own contents already
407
+ * end cleanly. Two refusals keep this honest:
408
+ *
409
+ * - Cut inside a string literal (an unterminated `"`) returns undefined. The
410
+ * value itself is incomplete, so any closing brace would silently shorten
411
+ * it — and these arguments carry shell commands.
412
+ * - Nothing left open returns undefined, so a well-formed candidate never
413
+ * reaches this path.
414
+ *
415
+ * Appending a closer never adds or removes a value, so a caller whose payload
416
+ * parsed to a complete object here is getting back exactly what the model
417
+ * emitted, minus the previous call's echo.
418
+ */
419
+ function appendClosers(text: string): string | undefined {
420
+ const open: string[] = [];
421
+ let inString = false;
422
+ let escaped = false;
423
+ for (const ch of text) {
424
+ if (inString) {
425
+ if (escaped) escaped = false;
426
+ else if (ch === "\\") escaped = true;
427
+ else if (ch === '"') inString = false;
428
+ continue;
429
+ }
430
+ if (ch === '"') inString = true;
431
+ else if (ch === "{" || ch === "[") open.push(ch === "{" ? "}" : "]");
432
+ else if (ch === "}" || ch === "]") open.pop();
433
+ }
434
+ // Inside a string at the end: the value was cut mid-token, so refuse.
435
+ if (inString || open.length === 0) return undefined;
436
+ return text + open.reverse().join("");
437
+ }
385
438
  /**
386
439
  * Repair one tool call's arguments against its declared schema. `name` is the
387
440
  * downstream (restored) caller tool name. Returns the input string untouched