pi-freeflow 1.38.5 → 1.40.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 +39 -0
- package/README.md +49 -45
- package/package.json +5 -5
- package/src/atomic-write.ts +55 -0
- package/src/catalog.ts +35 -31
- package/src/client.ts +38 -44
- package/src/cline-accounts.ts +174 -57
- package/src/cline-device-auth.ts +48 -8
- package/src/commands.ts +110 -83
- package/src/config.ts +20 -2
- package/src/daemon.ts +1 -1
- package/src/health.ts +44 -6
- package/src/index.ts +7 -1
- package/src/lease.ts +134 -116
- package/src/models.ts +157 -155
- package/src/opencode-fingerprint.ts +42 -36
- package/src/probe.ts +2 -2
- package/src/proxy.ts +178 -30
- package/src/relay-state.ts +11 -40
- package/src/relay.ts +106 -17
- package/src/responses.ts +2 -1
- package/src/stream-pipe.ts +208 -41
- package/src/system-ca-fetch.ts +49 -14
- package/src/token.ts +96 -0
- package/src/tool-args.ts +29 -2
- package/src/tool-translation.ts +4 -41
- package/src/types.ts +1 -1
- package/src/update-checker.ts +18 -70
- package/src/update-core.ts +206 -0
- package/src/updater.ts +53 -36
- package/src/upstream-health.ts +28 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.40.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Streaming turns now survive a relay rate limit: a 429 error frame that arrives before any tool call is held, the relay is cooled, and the same turn resumes once on a sibling relay. Pre-stream 429s still roll before any client bytes, and tool-call turns keep the authentic verdict to avoid duplicate execution.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Reconcile the remaining free-model specs with the highest published twin values, keeping lane hard caps authoritative:
|
|
12
|
+
|
|
13
|
+
- Cat Jam now advertises its 1,048,576-token window: the endpoint refuses a 1.05M prompt with `context_length_exceeded` (max 1,048,576), and a 900,337-token needle probe recalled correctly.
|
|
14
|
+
- Nemotron 3 Ultra (both lanes) now declares the 1,000,000-token lane cap; larger prompts are refused.
|
|
15
|
+
- Nemotron 3.5 and Super now carry the highest published 235,929 output ceiling below the window.
|
|
16
|
+
- Thinking selectors now expose only the depths published by the free or paid twin's datasheet; unlisted rungs stay locked off. The 3.6/3.8 Qwens add `minimal`, the other OVH reasoning rows expose `medium`, Inkling Small adds `max`, and Exo follows its paid twin's `minimal..xhigh` union.
|
|
17
|
+
- OVH output ceilings follow the highest published twin value below the window: 235,929 for the 397B and 3.8 Qwens, 65,536 for the 9B, 3.6 and both gpt-oss models, and 32,768 for Mistral and Llama.
|
|
18
|
+
- Remove Space Bunny: the free endpoint answers `410 ModelDeprecated` and points at `minimax-m3.1-flash-free`.
|
|
19
|
+
- Declare only text and image inputs. The host catalog has no video or audio input type, so upstream video/audio/PDF support (MiMo, GLM 5.3 Flash, Nemotron Super VL, Nano Omni) is documented in the README instead of being advertised in the picker.
|
|
20
|
+
- Rate-limit rotation is now silent: a relay that answers 429 is cooled and deprioritized without a per-relay warning toast or per-relay WARN line while the pool still has a candidate. A pool-wide 429 burst alert still warns when many relays rate-limit in one minute. The rate-limit message appears only when no relay or direct fallback served the turn, including the existing 403-salvage path.
|
|
21
|
+
- Model specs refreshed against the latest upstream catalogs. Free and paid variants of the same model now share the highest published context window, output ceiling, and input modalities, so a picker entry no longer understates what the model accepts.
|
|
22
|
+
- Drop two Kilo models that stopped answering, so the picker only offers what is actually served: `nvidia/nemotron-3-super-120b-a12b:free` answers `404 Function ... Not found for account ...` from its Nvidia provider, and `thinkingmachines/inkling-small:free` answers `404 The requested model is currently unavailable`. Both verdicts reproduce from two independent egress IPs while the sibling Nvidia ids answer `200` with the same request shape. The catalog is 32 models; both ids stay on the retired list so a stale cache cannot bring them back.
|
|
23
|
+
|
|
24
|
+
## 1.39.0
|
|
25
|
+
|
|
26
|
+
### Minor Changes
|
|
27
|
+
|
|
28
|
+
- 7427771: Security, wire-path and automation fixes from the October 2026 review:
|
|
29
|
+
|
|
30
|
+
- The local proxy now requires its own access token: every `/v1/*` and `/_*` request must carry `Authorization: Bearer <token>` (the token is written to the data directory on first start and the plugin supplies it automatically), the Host header must be loopback, and the daemon refuses to kill a process it cannot identify as its own. `GET /health` stays open for liveness checks. External tools that call the proxy directly must read the token file and send the header.
|
|
31
|
+
- A request for one model is never silently served by another: the internal Kilo 400 retry no longer substitutes a different model, so upstream verdicts reach the caller unchanged.
|
|
32
|
+
- Answers are no longer corrupted before they reach the client: a plain JSON reply that merely mentions `data:` is forwarded byte-identically instead of being replaced by an empty completion, a complete final SSE event that lost its trailing blank line is no longer dropped, and malformed tool arguments can no longer stall the proxy with a quadratic scan.
|
|
33
|
+
- Relay health reflects reality: an upstream 404 or 401 forwarded by a healthy relay no longer cools the pool, and a repeated 500 no longer resets the failing relay's health or steals the sticky slot.
|
|
34
|
+
- Cline logins: a rejected grant is parked instead of re-tried on every turn, a failed save is reported instead of being shown as saved, and logging out no longer leaves the removed token in the recovery file.
|
|
35
|
+
- Startup hooks and updates: the Windows logon task stores a command Windows can launch, a failed `systemctl --user enable` is reported, and auto-update no longer blocks the host while installing.
|
|
36
|
+
- Automation: the free-model health check has a timeout that fits a fully hanging catalog, a new guardrail keeps that budget honest, drift alerts close themselves once recovered, and the published-tarball check now also fails when the package ships a file the tree does not have.
|
|
37
|
+
|
|
38
|
+
### Patch Changes
|
|
39
|
+
|
|
40
|
+
- 152862b: Retire Muse Spark 1.2 contributor from the OpenCode Zen lineup — the model was deprecated upstream and now answers HTTP 410. The catalog, docs, and health checks move to the remaining 35 live models (12 on Zen), and the retired id is dead-listed so an old disk cache or an upstream merge cannot bring it back into the picker.
|
|
41
|
+
|
|
3
42
|
## 1.38.5
|
|
4
43
|
|
|
5
44
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# pi-freeflow
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 32 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
|
|
|
7
7
|
[](https://www.npmjs.com/package/pi-freeflow)
|
|
8
8
|
[](https://www.npmjs.com/package/pi-freeflow)
|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
10
|
-
[](https://github.com/
|
|
11
|
-
[](https://github.com/
|
|
10
|
+
[](https://github.com/earendil-works/pi)
|
|
11
|
+
[](https://github.com/can1357/oh-my-pi)
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -16,11 +16,11 @@ 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
|
-
| **
|
|
19
|
+
| **32 free models** | 11 from OpenCode Zen, 11 from KiloCode Gateway, 2 from Cline, 8 from OVHcloud AI Endpoints, context windows up to 1M. Full list below. | **$0** |
|
|
20
20
|
| **Relay pool** | Route requests through your own relays: a self-hosted egress pool (many egress IPs on one small host) or Cloudflare Workers / Deno Deploy / Vercel Edge workers. 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 host or 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** |
|
|
23
|
-
| **Shared local proxy** | One daemon on `127.0.0.1:28180` serves every session on the machine, so parallel subagents reuse it instead of opening their own connections. | **$0** |
|
|
23
|
+
| **Shared local proxy** | One daemon on `127.0.0.1:28180` serves every session on the machine, so parallel subagents reuse it instead of opening their own connections. Loopback-only and token-gated: hosts send the token automatically, external tools read `~/.pi/agent/token`. | **$0** |
|
|
24
24
|
| **Logs you can read** | `~/.pi/agent/pi-freeflow.log`, rotated at 10MB. Tail it with `/freeflow logs`. | **$0** |
|
|
25
25
|
|
|
26
26
|
---
|
|
@@ -48,13 +48,13 @@ pi install npm:pi-freeflow
|
|
|
48
48
|
**OMP, interactive:**
|
|
49
49
|
```bash
|
|
50
50
|
omp
|
|
51
|
-
/model → freeflow →
|
|
51
|
+
/model → freeflow → deepseek-v4.1-flash-free (1M) → max
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
**OMP, one shot:**
|
|
55
55
|
```bash
|
|
56
|
-
omp -p --model freeflow/
|
|
57
|
-
omp -p --model freeflow/
|
|
56
|
+
omp -p --model freeflow/deepseek-v4.1-flash-free "build me a SaaS"
|
|
57
|
+
omp -p --model freeflow/longcat:high "solve this bug" # alias + thinking level
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
**Pi, interactive:**
|
|
@@ -65,7 +65,7 @@ pi
|
|
|
65
65
|
|
|
66
66
|
**Pi, one shot:**
|
|
67
67
|
```bash
|
|
68
|
-
pi -p --model freeflow/
|
|
68
|
+
pi -p --model freeflow/longcat:high "solve this bug"
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
Model IDs accept a full canonical ID, a short alias (see the tables below),
|
|
@@ -183,6 +183,7 @@ The same command set works identically in OMP and Pi:
|
|
|
183
183
|
|
|
184
184
|
```bash
|
|
185
185
|
/freeflow status # View active relay, pool status, and candidates
|
|
186
|
+
/freeflow hide|show # Hide or restore the status-bar widget (also `widget hide`/`widget show`)
|
|
186
187
|
/freeflow list # List all relays with real-time health badges
|
|
187
188
|
/freeflow use <url|index|label> # Switch active relay
|
|
188
189
|
/freeflow url <url> # Set the active relay URL directly
|
|
@@ -190,12 +191,16 @@ The same command set works identically in OMP and Pi:
|
|
|
190
191
|
/freeflow label <index|url> <name># Assign a friendly label to a relay
|
|
191
192
|
/freeflow remove <index|url|label># Remove a relay from the pool
|
|
192
193
|
/freeflow test <index|url|label> # Probe a relay for reachability and latency
|
|
193
|
-
/freeflow on|off|auto|spread
|
|
194
|
+
/freeflow on|off|auto|sticky|spread # Relay mode (auto/sticky = sticky primary, spread = rotate pool)
|
|
194
195
|
/freeflow deploy <platform> # Guided relay deploy: vercel|cloudflare|deno, token in-memory, auto-adds
|
|
195
196
|
/freeflow logs [lines] # Inspect recent proxy logs
|
|
196
197
|
/freeflow trace [req-id] # Tail logs filtered by request correlation ID
|
|
197
198
|
/freeflow refresh # Reload the model catalog from live upstreams
|
|
199
|
+
/freeflow reload models # Alias of refresh
|
|
198
200
|
/freeflow update # Check for and install a package update
|
|
201
|
+
/freeflow autoupdate [on|off] # View or toggle automatic background updates
|
|
202
|
+
/freeflow install-startup # Install the login-startup hook so the daemon comes up with your session
|
|
203
|
+
/freeflow uninstall-startup # Remove the login-startup hook
|
|
199
204
|
/freeflow debug on | off # Toggle full HTTP lifecycle debug logging
|
|
200
205
|
/freeflow kill # Stop the shared proxy daemon now (restarts on next use)
|
|
201
206
|
/freeflow cline login # Sign in through the browser, save to the per-user pool
|
|
@@ -208,52 +213,51 @@ The same command set works identically in OMP and Pi:
|
|
|
208
213
|
|
|
209
214
|
---
|
|
210
215
|
|
|
211
|
-
###
|
|
216
|
+
### 32 models, one command
|
|
212
217
|
|
|
213
218
|
```bash
|
|
214
219
|
/model → freeflow → pick
|
|
215
220
|
```
|
|
216
221
|
|
|
217
|
-
Every model specification is reconciled against [models.dev](https://models.dev/), [OpenRouter](https://openrouter.ai/), and OMP's built-in catalog
|
|
218
|
-
|
|
222
|
+
Every model specification is reconciled against [models.dev](https://models.dev/), [OpenRouter](https://openrouter.ai/), and OMP's built-in catalog on 2026-10-20: a free and a non-free variant of the same model share the highest published context window, output ceiling, and input modalities across its twins. The catalog follows that published spec even where a free lane enforces a lower runtime cap; the authentic upstream error is surfaced above it. (Measured runtime caps: MiMo ~348K, GLM 5.3 Flash ~297K, Laguna S 262K.) Lane-published hard caps stay authoritative where the lane refuses above them: OVH's advertised context windows, the 1,000,000-token Nemotron 3 Ultra ceiling, and Dots3-Note's 393,216 served window (its only provider refused a 460,800 output budget on 2026-10-20).
|
|
223
|
+
|
|
224
|
+
The host catalog carries text and image inputs only. Upstream video and audio support (MiMo: audio + video; GLM 5.3 Flash: video + PDF; Nemotron Super VL and Nano Omni: video) is therefore not selectable from the picker and is not declared in `input`.
|
|
225
|
+
|
|
226
|
+
#### OpenCode Zen (11 models), Chat Completions API
|
|
219
227
|
|
|
220
228
|
Good defaults for long coding sessions and agentic work.
|
|
221
229
|
|
|
222
230
|
| Model ID | Creator / Lab | Context | Max Output | Thinking | Vision |
|
|
223
231
|
| :--- | :--- | :--- | :--- | :--- | :--- |
|
|
224
|
-
| `cat-jam-free` | Cat Jam | **
|
|
225
|
-
| `
|
|
226
|
-
| `
|
|
227
|
-
| `nemotron-3.5-
|
|
228
|
-
| `nemotron-3
|
|
229
|
-
| `nemotron-3-ultra-free` | NVIDIA | **1M** (1.000.000) | **128K** (128.000) | `minimal … xhigh` | ❌ |
|
|
232
|
+
| `cat-jam-free` | Cat Jam | **1M** (1.048.576) | **131K** (131.072) | `low … max` | ❌ |
|
|
233
|
+
| `mimo-v2.6-flash-free` | Xiaomi MiMo | **1M** (1.048.576) | **128K** (131.072) | `minimal … xhigh`\* | ✅ |
|
|
234
|
+
| `nemotron-3.5-lightning-free` | NVIDIA | **1M** (1.000.000) | **235K** (235.929) | `minimal … xhigh` | ❌ |
|
|
235
|
+
| `nemotron-3.5-super-vl-free` | NVIDIA | **1M** (1.048.576) | **235K** (235.929) | `minimal … xhigh` | ✅ |
|
|
236
|
+
| `nemotron-3-ultra-free` | NVIDIA | **1M** (1.000.000) | **182K** (182.520) | `minimal … xhigh` | ❌ |
|
|
230
237
|
| `big-pickle` | Big Pickle | **200K** (200.000) | **32K** (32.000) | `high / max` | ❌ |
|
|
231
|
-
| `
|
|
232
|
-
| `
|
|
233
|
-
| `glm-5.3-flash-free` | Zhipu AI | **1M** (1.000.000) | **32K** (32.000) | `minimal … max` | ✅ |
|
|
238
|
+
| `longcat-2.5-preview-free` | Meituan LongCat | **1M** (1.048.576) | **131K** (131.072) | `minimal … xhigh` | ✅ |
|
|
239
|
+
| `glm-5.3-flash-free` | Zhipu AI | **1M** (1.048.576) | **131K** (131.072) | `low / high / max` | ✅ |
|
|
234
240
|
| `beam-free` | Reflection AI | **1M** (1.048.576) | **131K** (131.072) | `low … max` | ❌ |
|
|
235
|
-
| `exo-free` | Undisclosed | **1M** (1.048.576) | **131K** (131.072) | `
|
|
236
|
-
| `deepseek-v4.1-flash-free` | DeepSeek | **1M** (1.
|
|
241
|
+
| `exo-free` | Undisclosed | **1M** (1.048.576) | **131K** (131.072) | `minimal … xhigh` | ✅ |
|
|
242
|
+
| `deepseek-v4.1-flash-free` | DeepSeek | **1M** (1.048.576) | **393K** (393.216) | `low / high / max` | ✅ |
|
|
237
243
|
|
|
238
|
-
#### KiloCode Gateway (
|
|
244
|
+
#### KiloCode Gateway (11 models), OpenRouter compatible
|
|
239
245
|
|
|
240
246
|
Keyless access. Short aliases work for every row (the full ID is in parentheses).
|
|
241
247
|
|
|
242
248
|
| Model ID | Creator / Lab | Context | Max Output | Thinking | Vision |
|
|
243
249
|
| :--- | :--- | :--- | :--- | :--- | :--- |
|
|
244
|
-
| `dots-3-note-preview` (`dots-studio/...:free`) | Dots Studio | **
|
|
245
|
-
| `nemotron-3-nano-omni` (`nvidia/...:free`) | NVIDIA | **
|
|
246
|
-
| `nemotron-3-ultra-550b` (`nvidia/...:free`) | NVIDIA | **1M** (1.000.000) | **
|
|
247
|
-
| `nvidia/nemotron-3.5-lightning:free` | NVIDIA | **1M** (1.000.000) | **
|
|
248
|
-
| `nemotron-3-super` (`nvidia/...:free`) | NVIDIA | **262K** (262.144) | **262K** (262.144) | `minimal…xhigh`\* | ❌ |
|
|
250
|
+
| `dots-3-note-preview` (`dots-studio/...:free`) | Dots Studio | **384K** (393.216) | **256K** (262.144) | `minimal…xhigh`\* | ✅ |
|
|
251
|
+
| `nemotron-3-nano-omni` (`nvidia/...:free`) | NVIDIA | **262K** (262.144) | **65K** (65.536) | `minimal…xhigh`\* | ✅ |
|
|
252
|
+
| `nemotron-3-ultra-550b` (`nvidia/...:free`) | NVIDIA | **1M** (1.000.000) | **182K** (182.520) | `minimal…xhigh`\* | ❌ |
|
|
253
|
+
| `nvidia/nemotron-3.5-lightning:free` | NVIDIA | **1M** (1.000.000) | **235K** (235.929) | `minimal…xhigh`\* | ❌ |
|
|
249
254
|
| `north-mini-code` (`cohere/...:free`) | Cohere | **256K** (256.000) | **64K** (64.000) | `minimal…xhigh`\* | ❌ |
|
|
250
|
-
| `laguna-s-2.1:free` (`poolside/...:free`) | Poolside | **
|
|
255
|
+
| `laguna-s-2.1:free` (`poolside/...:free`) | Poolside | **1M** (1.048.576) | **131K** (131.072) | `minimal…xhigh`\* | ❌ |
|
|
251
256
|
| `laguna-xs-2.1:free` (`poolside/...:free`) | Poolside | **262K** (262.144) | **32K** (32.768) | `minimal…xhigh`\* | ❌ |
|
|
252
|
-
| `lfm-2.5` (`liquid/lfm-2.5-2.6b:free`) | Liquid AI | **
|
|
257
|
+
| `lfm-2.5` (`liquid/lfm-2.5-2.6b:free`) | Liquid AI | **128K** (128.000) | **32K** (32.768) | `minimal…xhigh`\* | ❌ |
|
|
253
258
|
| `kilo-auto` (`kilo-auto/free`) | Kilo Gateway Auto | **256K** (256.000) | **10K** (10.000) | `minimal…xhigh`\* | ❌ |
|
|
254
259
|
| `openrouter` (`openrouter/free`) | OpenRouter Free | **200K** (200.000) | **65K** (65.536) | `minimal…xhigh`\* | ✅ |
|
|
255
260
|
| `glyph-cluster` (`stealth/glyph-cluster`) | Stealth preview (lab undisclosed) | **256K** (256.000) | **256K** (256.000) | `minimal…xhigh`\* | ❌ |
|
|
256
|
-
| `inkling-small` (`thinkingmachines/inkling-small:free`) | Thinking Machines | **1M** (1.048.576) | **262K** (262.144) | `minimal…xhigh`\* | ✅ |
|
|
257
261
|
|
|
258
262
|
\* Levels are forwarded as-is through the OpenRouter-style nested `reasoning` parameter; effort mapping is decided by each model. MiMo collapses `minimal→low` and `xhigh→high` upstream, so its selector shows 5 labels but only 3 distinct effort values.
|
|
259
263
|
|
|
@@ -273,19 +277,19 @@ Each account can only be saved once: an extra login for an account you already h
|
|
|
273
277
|
|
|
274
278
|
Anonymous access — no account, no key, nothing to sign in to. Direct only (never through the relay pool), because OVH has no per-IP gate for relays to route around. OVH rate-limits anonymous callers per IP, so a burst of parallel subagents can hit `429 API rate limit exceeded`; the model list itself never costs anything.
|
|
275
279
|
|
|
276
|
-
Pick them with the slash-free short name (the full ID is in parentheses).
|
|
280
|
+
Pick them with the slash-free short name (the full ID is in parentheses). The lane's rows publish `low`/`medium`/`high`; the 3.6 and 3.8 Qwens add `minimal`, and nothing on this lane offers `xhigh`/`max`.
|
|
277
281
|
All eight talk the normal chat API. Hosts that prefer the responses API work too — the proxy maps that shape to chat and maps the answer back, so tool calls and streamed output behave exactly as they do on the other providers.
|
|
278
282
|
|
|
279
283
|
| Model ID | Creator / Lab | Context | Max Output | Thinking | Vision |
|
|
280
284
|
| :--- | :--- | :--- | :--- | :--- | :--- |
|
|
281
|
-
| `gpt-oss-120b` (`ovh-free/gpt-oss-120b`) | OpenAI | **128K** (131.072) | **
|
|
282
|
-
| `gpt-oss-20b` (`ovh-free/gpt-oss-20b`) | OpenAI | **128K** (131.072) | **
|
|
283
|
-
| `qwen3.5-397b` (`ovh-free/Qwen3.5-397B-A17B`) | Alibaba Qwen | **256K** (262.144) | **
|
|
284
|
-
| `qwen3.5-9b` (`ovh-free/Qwen3.5-9B`) | Alibaba Qwen | **256K** (262.144) | **
|
|
285
|
-
| `qwen3.6-27b` (`ovh-free/Qwen3.6-27B`) | Alibaba Qwen | **256K** (262.144) | **
|
|
286
|
-
| `qwen3.8-27b` (`ovh-free/Qwen3.8-27B`) | Alibaba Qwen | **256K** (262.144) | **
|
|
287
|
-
| `mistral-small-3.2` (`ovh-free/Mistral-Small-3.2-24B-Instruct-2506`) | Mistral AI | **128K** (131.072) | **
|
|
288
|
-
| `llama-3.3-70b` (`ovh-free/Meta-Llama-3_3-70B-Instruct`) | Meta | **128K** (131.072) | **
|
|
285
|
+
| `gpt-oss-120b` (`ovh-free/gpt-oss-120b`) | OpenAI | **128K** (131.072) | **65K** (65.536) | `low/medium/high` | ❌ |
|
|
286
|
+
| `gpt-oss-20b` (`ovh-free/gpt-oss-20b`) | OpenAI | **128K** (131.072) | **65K** (65.536) | `low/medium/high` | ❌ |
|
|
287
|
+
| `qwen3.5-397b` (`ovh-free/Qwen3.5-397B-A17B`) | Alibaba Qwen | **256K** (262.144) | **235K** (235.929) | `low/medium/high` | ✅ |
|
|
288
|
+
| `qwen3.5-9b` (`ovh-free/Qwen3.5-9B`) | Alibaba Qwen | **256K** (262.144) | **65K** (65.536) | `low/medium/high` | ✅ |
|
|
289
|
+
| `qwen3.6-27b` (`ovh-free/Qwen3.6-27B`) | Alibaba Qwen | **256K** (262.144) | **65K** (65.536) | `minimal … high` | ✅ |
|
|
290
|
+
| `qwen3.8-27b` (`ovh-free/Qwen3.8-27B`) | Alibaba Qwen | **256K** (262.144) | **235K** (235.929) | `minimal … high` | ✅ |
|
|
291
|
+
| `mistral-small-3.2` (`ovh-free/Mistral-Small-3.2-24B-Instruct-2506`) | Mistral AI | **128K** (131.072) | **32K** (32.768) | ❌ | ✅ |
|
|
292
|
+
| `llama-3.3-70b` (`ovh-free/Meta-Llama-3_3-70B-Instruct`) | Meta | **128K** (131.072) | **32K** (32.768) | ❌ | ❌ |
|
|
289
293
|
|
|
290
294
|
---
|
|
291
295
|
|
|
@@ -305,14 +309,14 @@ cat ~/.pi/agent/pi-freeflow.log | tail -n 50
|
|
|
305
309
|
|
|
306
310
|
This package stays thin. It ships three things: a model catalog, a relay proxy, and a log. There is no build step and one runtime dependency (`undici`). Thinking and prompt normalization stay with the host (`pi-ai`).
|
|
307
311
|
|
|
308
|
-
About
|
|
312
|
+
About 52k lines including tests: `src/` is roughly 19.4k across 30 files, and the 90 test files add roughly 32.4k more. The full suite (sandboxed, network-mocked) and typecheck pass before every release. See CHANGELOG.md.
|
|
309
313
|
|
|
310
314
|
---
|
|
311
315
|
|
|
312
316
|
### FAQ
|
|
313
317
|
|
|
314
318
|
**Do I need API keys?**
|
|
315
|
-
No. OpenCode and Kilo free models
|
|
319
|
+
No. OpenCode and Kilo free models need no key of your own. The proxy strips the placeholder credential the host provider registers with — forwarding it upstream gets a `401` (for Kilo, `Bearer kilo-free` answers `401 INVALID_TOKEN`) — so your key, if any, never leaves the machine. For OpenCode Zen it then adds the fixed `Authorization: Bearer public` the current `/inference/v1` gateway requires; the Kilo and OVHcloud lanes go out with no `Authorization` header at all. You never paste a key. Cline free models need a browser login instead: `/freeflow cline login` saves it to a per-user pool on your machine. OVHcloud AI Endpoints models need no account and no key at all.
|
|
316
320
|
|
|
317
321
|
**What if all relays hit rate limits?**
|
|
318
322
|
The proxy tries direct upstream. If that is also rate-limited, the host shows the limit. That number is the shared upstream cap; without relays you would hit the same wall sooner.
|
|
@@ -378,7 +382,7 @@ pnpm smoke # verifies extensions/index.ts loads without crashing
|
|
|
378
382
|
#### Project structure
|
|
379
383
|
|
|
380
384
|
```
|
|
381
|
-
├── models.ts #
|
|
385
|
+
├── models.ts # 32-model catalog definitions
|
|
382
386
|
├── index.ts # extension entry, lifecycle hooks
|
|
383
387
|
├── catalog.ts # model catalog cache (24h disk)
|
|
384
388
|
├── proxy.ts # local proxy server (127.0.0.1:28180)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-freeflow",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.40.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",
|
|
@@ -52,13 +52,13 @@
|
|
|
52
52
|
"docs:build": "vitepress build docs"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@changesets/cli": "^
|
|
55
|
+
"@changesets/cli": "^3.0.3",
|
|
56
56
|
"@types/node": "^22.13.9",
|
|
57
|
-
"js-yaml": "^
|
|
58
|
-
"typescript": "^
|
|
57
|
+
"js-yaml": "^5.5.0",
|
|
58
|
+
"typescript": "^7.0.2",
|
|
59
59
|
"vitepress": "^1.6.4"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"undici": "7.30.
|
|
62
|
+
"undici": "7.30.1"
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
|
|
4
|
+
/** Block synchronously for ms without burning CPU. */
|
|
5
|
+
function sleepSync(ms: number): void {
|
|
6
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Rename with bounded retry: Windows briefly locks the target when another
|
|
11
|
+
* process holds the file open, surfacing as EPERM/EACCES/EBUSY. Three attempts
|
|
12
|
+
* with a fixed 50ms backoff by default; any other error fails immediately.
|
|
13
|
+
*/
|
|
14
|
+
export function renameWithRetry(from: string, to: string, attempts = 3, delayMs = 50): void {
|
|
15
|
+
const retryable: Record<string, true> = { EPERM: true, EACCES: true, EBUSY: true };
|
|
16
|
+
for (let attempt = 1; ; attempt++) {
|
|
17
|
+
try {
|
|
18
|
+
fs.renameSync(from, to);
|
|
19
|
+
return;
|
|
20
|
+
} catch (e) {
|
|
21
|
+
const code = (e as NodeJS.ErrnoException | null)?.code;
|
|
22
|
+
if (!code || !retryable[code] || attempt >= attempts) {
|
|
23
|
+
throw e;
|
|
24
|
+
}
|
|
25
|
+
sleepSync(delayMs);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Write `data` to `file` atomically: stage the bytes in a sibling temp file,
|
|
32
|
+
* then rename over the target, so a crash leaves either the old or the new
|
|
33
|
+
* file intact. The staged `<file>.<uuid>.tmp` is deliberately left in place
|
|
34
|
+
* when the rename never lands — it is the only copy of the bytes that failed
|
|
35
|
+
* to land, and relay-state's cleanupStaleTmpFiles reaps stale litter later.
|
|
36
|
+
*
|
|
37
|
+
* `mode` is applied to the staged file and re-applied to the final path
|
|
38
|
+
* (best-effort — Windows ignores POSIX modes), so secret-bearing files keep
|
|
39
|
+
* owner-only permissions.
|
|
40
|
+
*/
|
|
41
|
+
export function writeFileAtomic(file: string, data: string, opts?: { mode?: number }): void {
|
|
42
|
+
const tmp = `${file}.${randomUUID()}.tmp`;
|
|
43
|
+
fs.writeFileSync(tmp, data, opts?.mode ? { encoding: "utf8", mode: opts.mode } : "utf8");
|
|
44
|
+
if (opts?.mode) {
|
|
45
|
+
try {
|
|
46
|
+
fs.chmodSync(tmp, opts.mode);
|
|
47
|
+
} catch { }
|
|
48
|
+
}
|
|
49
|
+
renameWithRetry(tmp, file);
|
|
50
|
+
if (opts?.mode) {
|
|
51
|
+
try {
|
|
52
|
+
fs.chmodSync(file, opts.mode);
|
|
53
|
+
} catch { }
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/catalog.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Provides 24-hour atomic disk caching with safe offline fallback to verified static definitions.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { randomUUID } from "node:crypto";
|
|
8
7
|
import fs from "node:fs";
|
|
9
8
|
import path from "node:path";
|
|
9
|
+
import { writeFileAtomic } from "./atomic-write.ts";
|
|
10
10
|
import {
|
|
11
11
|
CATALOG_CACHE_FILE,
|
|
12
12
|
CATALOG_CACHE_TTL_MS,
|
|
@@ -101,6 +101,9 @@ export const DEAD_MODEL_IDS = new Set<string>([
|
|
|
101
101
|
"inclusionai/ling-3.0-flash-fin:free",
|
|
102
102
|
"mimo-v2.5-free",
|
|
103
103
|
"stealth/space-bunny-alpha",
|
|
104
|
+
// space-bunny-free: deprecated on the Zen free tier (2026-10-20) — the
|
|
105
|
+
// endpoint answers 410 ModelDeprecated and points at minimax-m3.1-flash-free.
|
|
106
|
+
"space-bunny-free",
|
|
104
107
|
"qwen/qwen3.8-27b:free",
|
|
105
108
|
"nvidia/nemotron-3.5-content-safety:free",
|
|
106
109
|
"fledge-alpha-free",
|
|
@@ -130,6 +133,20 @@ export const DEAD_MODEL_IDS = new Set<string>([
|
|
|
130
133
|
// muse-spark-1.3-contributor-free: decommissioned upstream (2026-10-17) — gone from
|
|
131
134
|
// live Zen /v1/models list, answers HTTP 410 "Model muse-spark-1.3-contributor-free has been deprecated".
|
|
132
135
|
"muse-spark-1.3-contributor-free",
|
|
136
|
+
// muse-spark-1.2-contributor-free: decommissioned upstream (2026-10-18) — answers
|
|
137
|
+
// HTTP 410 "Model muse-spark-1.2-contributor-free has been deprecated" while the
|
|
138
|
+
// same request shape on the surviving Zen models answers 200. Retired from the
|
|
139
|
+
// static catalog the same day; the Cline Muse Spark 1.3 entry is unaffected.
|
|
140
|
+
"muse-spark-1.2-contributor-free",
|
|
141
|
+
// nvidia/nemotron-3-super-120b-a12b:free: dead route (2026-10-21) — Kilo answers
|
|
142
|
+
// 404 "Function '<uuid>': Not found for account ..." from its Nvidia provider on
|
|
143
|
+
// every attempt, from two independent egress IPs, while sibling Nvidia ids answer
|
|
144
|
+
// 200 with the same request shape.
|
|
145
|
+
"nvidia/nemotron-3-super-120b-a12b:free",
|
|
146
|
+
// thinkingmachines/inkling-small:free: gone upstream (2026-10-21) — Kilo answers
|
|
147
|
+
// HTTP 404 "The requested model is currently unavailable" (unavailable_model) on
|
|
148
|
+
// every attempt, from two independent egress IPs; it is no longer served.
|
|
149
|
+
"thinkingmachines/inkling-small:free",
|
|
133
150
|
]);
|
|
134
151
|
/**
|
|
135
152
|
* Free-tier allowlist for anything entering the picker via network or stale disk.
|
|
@@ -169,7 +186,7 @@ export function sanitizeCatalogModels(models: RegisteredModel[]): RegisteredMode
|
|
|
169
186
|
}
|
|
170
187
|
/**
|
|
171
188
|
* In-memory cache of currently active/available free models.
|
|
172
|
-
* Initialized with all
|
|
189
|
+
* Initialized with all 32 verified models for 0ms instant availability.
|
|
173
190
|
*/
|
|
174
191
|
let aliveCatalog: RegisteredModel[] = ALL_MODELS.map((m) => ({
|
|
175
192
|
...m,
|
|
@@ -288,10 +305,11 @@ export function enrichModelDef(raw: RawModelItem, source: Upstream): RegisteredM
|
|
|
288
305
|
maxTokens = 131_072;
|
|
289
306
|
}
|
|
290
307
|
|
|
291
|
-
// Muse Spark is served on the responses API upstream (
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
// muse-spark id off the chat
|
|
308
|
+
// Muse Spark is served on the responses API upstream (the Cline entry still
|
|
309
|
+
// declares it; the Zen 1.2/1.3 contributor ids were retired upstream). Every
|
|
310
|
+
// known id short-circuits above, so this only decides for ids Zen lists
|
|
311
|
+
// dynamically — matching the family keeps a future muse-spark id off the chat
|
|
312
|
+
// endpoint it would 500 on.
|
|
295
313
|
const isResponses = idLower.includes("muse-spark");
|
|
296
314
|
|
|
297
315
|
return {
|
|
@@ -333,39 +351,18 @@ export function readCatalogCache(): CatalogCacheData | null {
|
|
|
333
351
|
}
|
|
334
352
|
|
|
335
353
|
/**
|
|
336
|
-
* Atomically write catalog cache data to disk
|
|
354
|
+
* Atomically write catalog cache data to disk (temp file + rename, see
|
|
355
|
+
* src/atomic-write.ts).
|
|
337
356
|
* Persists etag alongside models for subsequent If-None-Match conditional requests.
|
|
338
357
|
* Note: fsync not needed — rename is atomic on same filesystem; crash leaves either old or new file intact.
|
|
339
358
|
*/
|
|
340
|
-
function sleepSync(ms: number): void {
|
|
341
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function renameWithRetry(from: string, to: string): void {
|
|
345
|
-
const retryable: Record<string, true> = { EPERM: true, EACCES: true, EBUSY: true };
|
|
346
|
-
for (let attempt = 1; ; attempt++) {
|
|
347
|
-
try {
|
|
348
|
-
fs.renameSync(from, to);
|
|
349
|
-
return;
|
|
350
|
-
} catch (e) {
|
|
351
|
-
const code = (e as NodeJS.ErrnoException | null)?.code;
|
|
352
|
-
if (!code || !retryable[code] || attempt >= 3) {
|
|
353
|
-
throw e;
|
|
354
|
-
}
|
|
355
|
-
sleepSync(50);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
359
|
export function writeCatalogCache(data: CatalogCacheData): void {
|
|
361
360
|
try {
|
|
362
361
|
const dir = path.dirname(CATALOG_CACHE_FILE);
|
|
363
362
|
if (!fs.existsSync(dir)) {
|
|
364
363
|
fs.mkdirSync(dir, { recursive: true });
|
|
365
364
|
}
|
|
366
|
-
|
|
367
|
-
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2), "utf8");
|
|
368
|
-
renameWithRetry(tmpPath, CATALOG_CACHE_FILE);
|
|
365
|
+
writeFileAtomic(CATALOG_CACHE_FILE, JSON.stringify(data, null, 2));
|
|
369
366
|
} catch (err) {
|
|
370
367
|
logWarn("Could not persist catalog cache to disk", { error: String(err) });
|
|
371
368
|
}
|
|
@@ -430,7 +427,14 @@ export async function refreshCatalog(force = false): Promise<RegisteredModel[]>
|
|
|
430
427
|
signal: AbortSignal.timeout(CATALOG_REFRESH_TIMEOUT_MS),
|
|
431
428
|
});
|
|
432
429
|
if (res.status === 304) {
|
|
433
|
-
// Not modified
|
|
430
|
+
// Not modified upstream, but the cached list is still the only record of
|
|
431
|
+
// dynamically discovered ids this process never merged (a 304 revalidates
|
|
432
|
+
// before any 200 merge), so overlay it exactly like the stale-cache paths
|
|
433
|
+
// below instead of returning the untouched in-memory catalog.
|
|
434
|
+
if (staleForEtag && Array.isArray(staleForEtag.models) && staleForEtag.models.length > 0) {
|
|
435
|
+
aliveCatalog = mergeCatalog(aliveCatalog, sanitizeCatalogModels(staleForEtag.models));
|
|
436
|
+
}
|
|
437
|
+
// Extend the timestamp to avoid a tight loop on the same 304
|
|
434
438
|
if (staleForEtag && Array.isArray(staleForEtag.models)) {
|
|
435
439
|
try {
|
|
436
440
|
writeCatalogCache({ ...staleForEtag, timestamp: Date.now() });
|