pi-freeflow 1.1.4 โ 1.1.6
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 +137 -23
- package/extensions/index.ts +37 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,10 +94,18 @@ omp plugin link /path/to/pi-freeflow
|
|
|
94
94
|
|
|
95
95
|
---
|
|
96
96
|
|
|
97
|
-
## ๐ Setting Up Your Own Egress Relays
|
|
97
|
+
## ๐ Setting Up Your Own Egress Relays (Multi-Cloud)
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
By default, FreeFlow works out of the box with direct upstream calls. To expand concurrency, bypass IP rate limits, and enable rolling failover, you can connect your own free cloud relays across **Cloudflare Workers**, **Vercel Edge**, **Deno Deploy**, or a **Linux VPS**.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
### 1. Cloudflare Worker Relay (Recommended โญ)
|
|
104
|
+
> **Free Tier**: 100,000 requests/day ยท Low latency ยท No 25-second execution timeout.
|
|
105
|
+
|
|
106
|
+
1. Go to [dash.cloudflare.com](https://dash.cloudflare.com) $\to$ **Workers & Pages** $\to$ **Create application** $\to$ **Create Worker**.
|
|
107
|
+
2. Name your worker (e.g. `my-freeflow-relay`) $\to$ click **Deploy**.
|
|
108
|
+
3. Click **Edit code**, paste this JavaScript, and click **Save and Deploy**:
|
|
101
109
|
|
|
102
110
|
```javascript
|
|
103
111
|
const ALLOWED_TARGETS = ["https://opencode.ai", "https://api.kilo.ai"];
|
|
@@ -108,8 +116,9 @@ export default {
|
|
|
108
116
|
return new Response(null, {
|
|
109
117
|
headers: {
|
|
110
118
|
"Access-Control-Allow-Origin": "*",
|
|
111
|
-
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
119
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS, HEAD",
|
|
112
120
|
"Access-Control-Allow-Headers": "*",
|
|
121
|
+
"Access-Control-Max-Age": "86400",
|
|
113
122
|
},
|
|
114
123
|
});
|
|
115
124
|
}
|
|
@@ -117,8 +126,8 @@ export default {
|
|
|
117
126
|
const target = request.headers.get("x-relay-target");
|
|
118
127
|
const relayPath = request.headers.get("x-relay-path") || "/";
|
|
119
128
|
|
|
120
|
-
if (!target || !ALLOWED_TARGETS.includes(target)) {
|
|
121
|
-
return new Response(JSON.stringify({ error: "Forbidden target" }), { status: 403 });
|
|
129
|
+
if (!target || !ALLOWED_TARGETS.includes(target.replace(/\/$/, ""))) {
|
|
130
|
+
return new Response(JSON.stringify({ error: "Forbidden target" }), { status: 403, headers: { "content-type": "application/json" } });
|
|
122
131
|
}
|
|
123
132
|
|
|
124
133
|
const targetUrl = target.replace(/\/$/, "") + relayPath;
|
|
@@ -126,42 +135,147 @@ export default {
|
|
|
126
135
|
headers.delete("x-relay-target");
|
|
127
136
|
headers.delete("x-relay-path");
|
|
128
137
|
headers.delete("host");
|
|
138
|
+
headers.delete("cf-connecting-ip");
|
|
139
|
+
headers.delete("cf-ray");
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const response = await fetch(targetUrl, {
|
|
143
|
+
method: request.method,
|
|
144
|
+
headers,
|
|
145
|
+
body: request.method !== "GET" && request.method !== "HEAD" ? request.body : undefined,
|
|
146
|
+
});
|
|
129
147
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
headers
|
|
133
|
-
|
|
134
|
-
|
|
148
|
+
const outHeaders = new Headers(response.headers);
|
|
149
|
+
outHeaders.set("Access-Control-Allow-Origin", "*");
|
|
150
|
+
return new Response(response.body, { status: response.status, headers: outHeaders });
|
|
151
|
+
} catch (err) {
|
|
152
|
+
return new Response(JSON.stringify({ error: "Upstream failed", details: String(err) }), { status: 502, headers: { "content-type": "application/json" } });
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
```
|
|
135
157
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
158
|
+
4. Copy your Worker URL and register it inside OMP:
|
|
159
|
+
```text
|
|
160
|
+
/freeflow use https://my-freeflow-relay.your-subdomain.workers.dev
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
### 2. Vercel Edge Relay
|
|
166
|
+
> **Free Tier**: 1,000,000 requests/month.
|
|
167
|
+
|
|
168
|
+
#### Method A: Automated In-Memory CLI Deploy
|
|
169
|
+
1. Generate a Vercel token at [vercel.com/account/tokens](https://vercel.com/account/tokens).
|
|
170
|
+
2. In OMP TUI, run:
|
|
142
171
|
```text
|
|
143
172
|
/freeflow deploy
|
|
144
173
|
```
|
|
145
|
-
*(Prompts for your
|
|
174
|
+
*(Prompts for your token in-memory, provisions a private Edge Function, and adds it to your relay pool automatically).*
|
|
175
|
+
|
|
176
|
+
#### Method B: Manual Git Deploy
|
|
177
|
+
Deploy a Git repository with these 3 files:
|
|
178
|
+
* **`api/relay.js`**:
|
|
179
|
+
```javascript
|
|
180
|
+
const ALLOWED_TARGETS = ["https://opencode.ai", "https://api.kilo.ai"];
|
|
181
|
+
export const config = { runtime: "edge" };
|
|
182
|
+
export default async function handler(req) {
|
|
183
|
+
const target = req.headers.get("x-relay-target");
|
|
184
|
+
const relayPath = req.headers.get("x-relay-path") || "/";
|
|
185
|
+
if (!target || !ALLOWED_TARGETS.includes(target.replace(/\/$/, ""))) return new Response(JSON.stringify({ error: "Forbidden" }), { status: 403 });
|
|
186
|
+
const targetUrl = target.replace(/\/$/, "") + relayPath;
|
|
187
|
+
const headers = new Headers(req.headers);
|
|
188
|
+
headers.delete("x-relay-target"); headers.delete("x-relay-path"); headers.delete("host");
|
|
189
|
+
const response = await fetch(targetUrl, { method: req.method, headers, body: req.method !== "GET" && req.method !== "HEAD" ? req.body : undefined, duplex: "half" });
|
|
190
|
+
return new Response(response.body, { status: response.status, headers: response.headers });
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
* **`package.json`**: `{ "name": "freeflow-relay", "version": "1.0.0" }`
|
|
194
|
+
* **`vercel.json`**: `{ "rewrites": [{ "source": "/(.*)", "destination": "/api/relay" }] }`
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
### 3. Deno Deploy Relay
|
|
199
|
+
> **Free Tier**: 100,000 requests/day.
|
|
200
|
+
|
|
201
|
+
1. Open [dash.deno.com](https://dash.deno.com) $\to$ **New Project** $\to$ **Playground**.
|
|
202
|
+
2. Paste the following TypeScript code:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
const ALLOWED_TARGETS = ["https://opencode.ai", "https://api.kilo.ai"];
|
|
206
|
+
|
|
207
|
+
Deno.serve(async (req) => {
|
|
208
|
+
if (req.method === "OPTIONS") {
|
|
209
|
+
return new Response(null, {
|
|
210
|
+
headers: {
|
|
211
|
+
"Access-Control-Allow-Origin": "*",
|
|
212
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
213
|
+
"Access-Control-Allow-Headers": "*",
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const target = req.headers.get("x-relay-target");
|
|
219
|
+
const relayPath = req.headers.get("x-relay-path") || "/";
|
|
220
|
+
if (!target || !ALLOWED_TARGETS.includes(target.replace(/\/$/, ""))) {
|
|
221
|
+
return new Response(JSON.stringify({ error: "Forbidden target" }), { status: 403 });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const targetUrl = target.replace(/\/$/, "") + relayPath;
|
|
225
|
+
const headers = new Headers(req.headers);
|
|
226
|
+
headers.delete("x-relay-target");
|
|
227
|
+
headers.delete("x-relay-path");
|
|
228
|
+
headers.delete("host");
|
|
229
|
+
|
|
230
|
+
const response = await fetch(targetUrl, {
|
|
231
|
+
method: req.method,
|
|
232
|
+
headers,
|
|
233
|
+
body: req.method !== "GET" && req.method !== "HEAD" ? req.body : undefined,
|
|
234
|
+
});
|
|
235
|
+
return new Response(response.body, { status: response.status, headers: response.headers });
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
3. Click **Deploy** and copy your Deno project URL (`https://project-name.deno.dev`).
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## ๐ฎ Managing Relays in FreeFlow
|
|
243
|
+
|
|
244
|
+
Inside the OMP / Pi TUI:
|
|
245
|
+
```text
|
|
246
|
+
/freeflow use <URL> # Set active relay and add to saved pool
|
|
247
|
+
/freeflow list # View all saved relays with status indicators (โ
= active)
|
|
248
|
+
/freeflow status # Check current relay health and egress mode
|
|
249
|
+
/freeflow refresh # Trigger live on-demand model catalog sync from upstream APIs
|
|
250
|
+
/freeflow on # Force enable relay egress mode
|
|
251
|
+
/freeflow off # Switch to direct upstream mode
|
|
252
|
+
/freeflow remove <URL> # Remove an inactive relay from pool
|
|
253
|
+
/freeflow logs # Show last 25 lines of debug logs
|
|
254
|
+
```
|
|
146
255
|
|
|
147
256
|
---
|
|
148
257
|
|
|
149
258
|
## ๐ ๏ธ Diagnostics & Troubleshooting
|
|
150
259
|
|
|
151
260
|
### Custom Port Configuration
|
|
152
|
-
By default, `pi-freeflow` uses a **Single Shared Port** (`18080`). All concurrent sub-agents automatically
|
|
261
|
+
By default, `pi-freeflow` uses a **Single Shared Master Port** (`18080`). All concurrent sub-agents automatically reuse this master proxy without spawning redundant servers or extra sockets.
|
|
153
262
|
|
|
154
263
|
To change the base port:
|
|
155
264
|
```env
|
|
156
265
|
FREEFLOW_PORT=19000
|
|
157
266
|
```
|
|
158
267
|
|
|
159
|
-
|
|
268
|
+
### Subagent Credentials
|
|
269
|
+
If subagents report missing provider keys, verify `~/.omp/agent/.env`:
|
|
270
|
+
```env
|
|
271
|
+
FREEFLOW_API_KEY=freeflow
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Live Logs
|
|
160
275
|
```bash
|
|
161
|
-
cat ~/.pi/agent/pi-freeflow.log | tail -n
|
|
162
|
-
# or inside OMP:
|
|
163
|
-
/freeflow logs
|
|
276
|
+
cat ~/.pi/agent/pi-freeflow.log | tail -n 40
|
|
164
277
|
```
|
|
278
|
+
|
|
165
279
|
---
|
|
166
280
|
|
|
167
281
|
## ๐ License
|
package/extensions/index.ts
CHANGED
|
@@ -1729,12 +1729,46 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1729
1729
|
pi.registerCommand("freeflow", commandSpec);
|
|
1730
1730
|
pi.registerCommand("bansos", commandSpec);
|
|
1731
1731
|
|
|
1732
|
-
|
|
1732
|
+
function updateStatusBar(ui?: ExtensionContext["ui"], providerName?: string) {
|
|
1733
|
+
if (!ui) return;
|
|
1734
|
+
if (relayState.enabled && relayState.relays.length > 0) {
|
|
1735
|
+
const label = shortRelayLabel(relayState.url);
|
|
1736
|
+
const idx = Math.max(
|
|
1737
|
+
1,
|
|
1738
|
+
relayState.relays.findIndex((r) => r.url === relayState.url) + 1,
|
|
1739
|
+
);
|
|
1740
|
+
const total = relayState.relays.length;
|
|
1741
|
+
ui.setStatus?.("freeflow", `relay: ON | ${label} ${idx}/${total}`);
|
|
1742
|
+
} else {
|
|
1743
|
+
ui.setStatus?.("freeflow", undefined);
|
|
1744
|
+
}
|
|
1745
|
+
ui.setStatus?.("bansos", undefined);
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// Reload persisted state on session start/resume and show status if active
|
|
1733
1749
|
pi.on?.("session_start", async (_event, ctx) => {
|
|
1734
1750
|
relayState = resolveRelayState();
|
|
1735
1751
|
statusUi = ctx.ui;
|
|
1736
|
-
ctx.ui
|
|
1737
|
-
|
|
1752
|
+
updateStatusBar(ctx.ui);
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
// Update status bar immediately when user switches models
|
|
1756
|
+
pi.on?.("model_select", async (event, ctx) => {
|
|
1757
|
+
statusUi = ctx.ui;
|
|
1758
|
+
const model =
|
|
1759
|
+
event && typeof event === "object" && "model" in event
|
|
1760
|
+
? event.model
|
|
1761
|
+
: null;
|
|
1762
|
+
const provider =
|
|
1763
|
+
model && typeof model === "object" && "provider" in model
|
|
1764
|
+
? String(model.provider)
|
|
1765
|
+
: null;
|
|
1766
|
+
if (provider === "freeflow") {
|
|
1767
|
+
updateStatusBar(ctx.ui, "freeflow");
|
|
1768
|
+
} else if (provider) {
|
|
1769
|
+
// Clear status when switched to other providers (e.g. anthropic, openai)
|
|
1770
|
+
ctx.ui?.setStatus?.("freeflow", undefined);
|
|
1771
|
+
}
|
|
1738
1772
|
});
|
|
1739
1773
|
|
|
1740
1774
|
// Pi normally pauses after threshold compaction. Queue a follow-up while the
|