pi-freeflow 1.4.2 → 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 +323 -279
- package/package.json +1 -1
- package/src/catalog.ts +250 -207
- package/src/commands.ts +670 -618
- package/src/config.ts +144 -145
- package/src/deploy.ts +496 -126
- package/src/index.ts +301 -277
- package/src/models.ts +355 -399
- package/src/proxy.ts +493 -500
- package/src/relay.ts +213 -210
- package/src/stream-pipe.ts +265 -263
package/src/index.ts
CHANGED
|
@@ -1,277 +1,301 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pi-freeflow — Modular, high-resiliency LLM extension for Pi & Oh My Pi (OMP)
|
|
3
|
-
*
|
|
4
|
-
* Provides access to
|
|
5
|
-
* - Single-port daemon reuse on 18080 across concurrent subagents
|
|
6
|
-
* - Multi-cloud rolling egress relays (Vercel Edge, Cloudflare, Deno)
|
|
7
|
-
* - 0ms instant startup with verified static catalog and background live health checks
|
|
8
|
-
* - Per-model thinking/reasoning translation and streaming SSE pass-through
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type * as http from "node:http";
|
|
12
|
-
import {
|
|
13
|
-
getAliveCatalog,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
export
|
|
40
|
-
export * from "./
|
|
41
|
-
export * from "./
|
|
42
|
-
export * from "./
|
|
43
|
-
export * from "./
|
|
44
|
-
export * from "./
|
|
45
|
-
export * from "./
|
|
46
|
-
export * from "./relay.ts";
|
|
47
|
-
export * from "./
|
|
48
|
-
export * from "./
|
|
49
|
-
export * from "./
|
|
50
|
-
export * from "./
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
let
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
"
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
logWarn("
|
|
169
|
-
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
ctx.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
1
|
+
/**
|
|
2
|
+
* pi-freeflow — Modular, high-resiliency LLM extension for Pi & Oh My Pi (OMP)
|
|
3
|
+
*
|
|
4
|
+
* Provides access to 21 free models (7 OpenCode Zen + 14 KiloCode Gateway) with:
|
|
5
|
+
* - Single-port daemon reuse on 18080 across concurrent subagents
|
|
6
|
+
* - Multi-cloud rolling egress relays (Vercel Edge, Cloudflare, Deno)
|
|
7
|
+
* - 0ms instant startup with verified static catalog and background live health checks
|
|
8
|
+
* - Per-model thinking/reasoning translation and streaming SSE pass-through
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type * as http from "node:http";
|
|
12
|
+
import {
|
|
13
|
+
getAliveCatalog,
|
|
14
|
+
mergeCatalog,
|
|
15
|
+
readCatalogCache,
|
|
16
|
+
refreshCatalog,
|
|
17
|
+
setAliveCatalog,
|
|
18
|
+
} from "./catalog.ts";
|
|
19
|
+
import { createCommandSpec, updateStatusBar } from "./commands.ts";
|
|
20
|
+
import { DEFAULT_HOST, HOST, PORT } from "./config.ts";
|
|
21
|
+
import { log, logInfo, logWarn } from "./logger.ts";
|
|
22
|
+
import { ALL_MODELS, KILO_MODEL_IDS, MODEL_MAP, getAllRegisteredModels, resolveCanonicalModelId } from "./models.ts";
|
|
23
|
+
import { isProxyAlive, startProxy } from "./proxy.ts";
|
|
24
|
+
import { resetRateLimits } from "./rate-limiter.ts";
|
|
25
|
+
import {
|
|
26
|
+
getActiveRelayState,
|
|
27
|
+
resolveRelayState,
|
|
28
|
+
setActiveRelayState,
|
|
29
|
+
setStatusUi,
|
|
30
|
+
setFreeFlowModelActive,
|
|
31
|
+
} from "./relay-state.ts";
|
|
32
|
+
import type {
|
|
33
|
+
ExtensionAPI,
|
|
34
|
+
ExtensionContext,
|
|
35
|
+
ProviderConfig,
|
|
36
|
+
RegisteredModel,
|
|
37
|
+
} from "./types.ts";
|
|
38
|
+
|
|
39
|
+
// Re-export all sub-modules for clean library and programmatic usage
|
|
40
|
+
export * from "./types.ts";
|
|
41
|
+
export * from "./config.ts";
|
|
42
|
+
export * from "./logger.ts";
|
|
43
|
+
export * from "./rate-limiter.ts";
|
|
44
|
+
export * from "./models.ts";
|
|
45
|
+
export * from "./catalog.ts";
|
|
46
|
+
export * from "./relay-state.ts";
|
|
47
|
+
export * from "./relay.ts";
|
|
48
|
+
export * from "./deploy.ts";
|
|
49
|
+
export * from "./stream-pipe.ts";
|
|
50
|
+
export * from "./proxy.ts";
|
|
51
|
+
export * from "./commands.ts";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Construct standard ProviderConfig for pi-ai / OMP registration.
|
|
55
|
+
*/
|
|
56
|
+
export function buildProviderConfig(
|
|
57
|
+
models: RegisteredModel[],
|
|
58
|
+
port: number = PORT,
|
|
59
|
+
): ProviderConfig {
|
|
60
|
+
return {
|
|
61
|
+
baseUrl: `http://${HOST}:${port}/v1`,
|
|
62
|
+
apiKey: "placeholder",
|
|
63
|
+
api: "openai-completions",
|
|
64
|
+
compat: { supportsDeveloperRole: false },
|
|
65
|
+
models: models.map((m) => {
|
|
66
|
+
const efforts = m.thinkingLevelMap
|
|
67
|
+
? (Object.keys(m.thinkingLevelMap) as (keyof typeof m.thinkingLevelMap)[]).filter(
|
|
68
|
+
(k) => m.thinkingLevelMap![k] !== null && k !== "off",
|
|
69
|
+
)
|
|
70
|
+
: ["minimal", "low", "medium", "high", "xhigh"];
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
id: m.id,
|
|
74
|
+
name: m.name,
|
|
75
|
+
api: m.api,
|
|
76
|
+
reasoning: m.reasoning,
|
|
77
|
+
thinking: m.reasoning
|
|
78
|
+
? {
|
|
79
|
+
mode: "effort",
|
|
80
|
+
efforts: efforts.length > 0 ? efforts : ["low", "high", "max"],
|
|
81
|
+
}
|
|
82
|
+
: undefined,
|
|
83
|
+
thinkingLevelMap: m.thinkingLevelMap,
|
|
84
|
+
input: m.input ?? ["text"],
|
|
85
|
+
contextWindow: m.contextWindow,
|
|
86
|
+
maxTokens: m.maxTokens,
|
|
87
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
88
|
+
compat: m.thinkingFormat
|
|
89
|
+
? {
|
|
90
|
+
supportsDeveloperRole: false,
|
|
91
|
+
thinkingFormat: m.thinkingFormat,
|
|
92
|
+
}
|
|
93
|
+
: m.api === "openai-responses"
|
|
94
|
+
? { sessionAffinityFormat: "openai-nosession" }
|
|
95
|
+
: m.source === "kilo"
|
|
96
|
+
? {
|
|
97
|
+
supportsDeveloperRole: false,
|
|
98
|
+
supportsReasoningEffort: false,
|
|
99
|
+
}
|
|
100
|
+
: {
|
|
101
|
+
supportsDeveloperRole: false,
|
|
102
|
+
supportsReasoningEffort: true,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Main extension entrypoint
|
|
111
|
+
*/
|
|
112
|
+
export default async function (pi: ExtensionAPI): Promise<void> {
|
|
113
|
+
logInfo("pi-freeflow extension initializing...");
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
let server: http.Server | null = null;
|
|
117
|
+
let actualPort = PORT;
|
|
118
|
+
|
|
119
|
+
// 2. Single-Port Shared Pattern: Check if daemon is already running (e.g. parent session)
|
|
120
|
+
const alreadyRunning = await isProxyAlive(PORT);
|
|
121
|
+
if (alreadyRunning) {
|
|
122
|
+
logInfo(
|
|
123
|
+
`Reusing existing pi-freeflow proxy daemon on http://${HOST}:${PORT}`,
|
|
124
|
+
);
|
|
125
|
+
actualPort = PORT;
|
|
126
|
+
} else {
|
|
127
|
+
try {
|
|
128
|
+
const r = await startProxy();
|
|
129
|
+
server = r.server;
|
|
130
|
+
actualPort = r.port;
|
|
131
|
+
} catch (e) {
|
|
132
|
+
log(
|
|
133
|
+
"error",
|
|
134
|
+
"extension inactive — could not bind proxy port. resolve the port conflict and restart pi.",
|
|
135
|
+
{ error: String(e) },
|
|
136
|
+
);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 3. Instant 0ms Static Catalog Registration
|
|
142
|
+
// Register static models immediately on boot so Pi/OMP picker is populated with zero latency!
|
|
143
|
+
const registeredCatalog: RegisteredModel[] = ALL_MODELS.map((m) => ({
|
|
144
|
+
...m,
|
|
145
|
+
source: KILO_MODEL_IDS.has(m.id) ? "kilo" : "opencode",
|
|
146
|
+
}));
|
|
147
|
+
setAliveCatalog(registeredCatalog);
|
|
148
|
+
const registerCatalog = (models: RegisteredModel[]): void => {
|
|
149
|
+
|
|
150
|
+
pi.registerProvider(
|
|
151
|
+
"freeflow",
|
|
152
|
+
buildProviderConfig(models, actualPort),
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
registerCatalog(registeredCatalog);
|
|
156
|
+
// Self-heal: if this session attached to an external daemon that later died,
|
|
157
|
+
// re-bind on the next lifecycle event so model calls recover without a restart.
|
|
158
|
+
let ensuringDaemon = false;
|
|
159
|
+
const ensureDaemon = async (): Promise<void> => {
|
|
160
|
+
if (ensuringDaemon) return;
|
|
161
|
+
ensuringDaemon = true;
|
|
162
|
+
try {
|
|
163
|
+
if (await isProxyAlive(actualPort)) return;
|
|
164
|
+
const r = await startProxy();
|
|
165
|
+
if (r.server) server = r.server;
|
|
166
|
+
if (r.port) actualPort = r.port;
|
|
167
|
+
registerCatalog(getAliveCatalog());
|
|
168
|
+
logWarn("proxy daemon was lost — re-bound locally", { port: actualPort });
|
|
169
|
+
} catch (e) {
|
|
170
|
+
logWarn("proxy daemon lost and re-bind failed", { error: String(e) });
|
|
171
|
+
} finally {
|
|
172
|
+
ensuringDaemon = false;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// 4. Background Catalog Refresh
|
|
177
|
+
// Asynchronously probe live upstreams and update the provider if alive model list changes
|
|
178
|
+
refreshCatalog(false)
|
|
179
|
+
.then((aliveModels) => {
|
|
180
|
+
if (aliveModels.length > 0) {
|
|
181
|
+
setAliveCatalog(aliveModels);
|
|
182
|
+
registerCatalog(mergeCatalog(registeredCatalog, aliveModels));
|
|
183
|
+
logInfo(
|
|
184
|
+
`Catalog refreshed: ${aliveModels.length} models verified active`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
.catch((err) => {
|
|
189
|
+
logWarn("Background catalog refresh failed; retaining static catalog", {
|
|
190
|
+
error: String(err),
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// 5. Register slash command
|
|
195
|
+
const commandSpec = createCommandSpec(pi, (updatedModels) => {
|
|
196
|
+
registerCatalog(updatedModels);
|
|
197
|
+
});
|
|
198
|
+
pi.registerCommand("freeflow", commandSpec);
|
|
199
|
+
|
|
200
|
+
// 6. Lifecycle Listeners
|
|
201
|
+
function isFreeFlowModelMatch(provider?: string, modelId?: string): boolean {
|
|
202
|
+
if (provider === "freeflow") return true;
|
|
203
|
+
if (typeof modelId === "string" && modelId.trim()) {
|
|
204
|
+
const clean = modelId.trim();
|
|
205
|
+
const canonical = resolveCanonicalModelId(clean);
|
|
206
|
+
return (
|
|
207
|
+
MODEL_MAP.has(clean) ||
|
|
208
|
+
MODEL_MAP.has(canonical) ||
|
|
209
|
+
getAliveCatalog().some((m) => m.id === clean || m.id === canonical)
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
// When provider and modelId are not yet populated on session_start,
|
|
213
|
+
// default to true so the widget is present when starting with freeflow
|
|
214
|
+
if (!provider && !modelId) {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
pi.on?.("session_start", async (_event, ctx: ExtensionContext) => {
|
|
221
|
+
await ensureDaemon();
|
|
222
|
+
const freshRelayState = resolveRelayState();
|
|
223
|
+
setStatusUi(ctx.ui);
|
|
224
|
+
|
|
225
|
+
let provider: string | undefined;
|
|
226
|
+
let modelId: string | undefined;
|
|
227
|
+
if (ctx && typeof ctx === "object" && "model" in ctx && ctx.model && typeof ctx.model === "object") {
|
|
228
|
+
const m = ctx.model;
|
|
229
|
+
if ("provider" in m && typeof m.provider === "string") {
|
|
230
|
+
provider = m.provider;
|
|
231
|
+
}
|
|
232
|
+
if ("id" in m && typeof m.id === "string") {
|
|
233
|
+
modelId = m.id;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const isFreeFlow = isFreeFlowModelMatch(provider, modelId);
|
|
237
|
+
setFreeFlowModelActive(isFreeFlow);
|
|
238
|
+
|
|
239
|
+
if (freshRelayState.mode !== "off") {
|
|
240
|
+
freshRelayState.enabled = freshRelayState.relays.length > 0;
|
|
241
|
+
setActiveRelayState(freshRelayState, false);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (isFreeFlow) {
|
|
245
|
+
updateStatusBar(ctx.ui);
|
|
246
|
+
} else {
|
|
247
|
+
ctx.ui?.setStatus?.("freeflow", undefined);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
pi.on?.("model_select", async (event, ctx: ExtensionContext) => {
|
|
252
|
+
await ensureDaemon();
|
|
253
|
+
const freshRelayState = resolveRelayState();
|
|
254
|
+
setStatusUi(ctx.ui);
|
|
255
|
+
let provider: string | undefined;
|
|
256
|
+
let modelId: string | undefined;
|
|
257
|
+
if (event && typeof event === "object" && "model" in event && event.model && typeof event.model === "object") {
|
|
258
|
+
const m = event.model;
|
|
259
|
+
if ("provider" in m && typeof m.provider === "string") {
|
|
260
|
+
provider = m.provider;
|
|
261
|
+
}
|
|
262
|
+
if ("id" in m && typeof m.id === "string") {
|
|
263
|
+
modelId = m.id;
|
|
264
|
+
}
|
|
265
|
+
} else if (ctx && typeof ctx === "object" && "model" in ctx && ctx.model && typeof ctx.model === "object") {
|
|
266
|
+
const m = ctx.model;
|
|
267
|
+
if ("provider" in m && typeof m.provider === "string") {
|
|
268
|
+
provider = m.provider;
|
|
269
|
+
}
|
|
270
|
+
if ("id" in m && typeof m.id === "string") {
|
|
271
|
+
modelId = m.id;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const isFreeFlow = isFreeFlowModelMatch(provider, modelId);
|
|
275
|
+
setFreeFlowModelActive(isFreeFlow);
|
|
276
|
+
|
|
277
|
+
if (freshRelayState.mode !== "off") {
|
|
278
|
+
freshRelayState.enabled = freshRelayState.relays.length > 0;
|
|
279
|
+
setActiveRelayState(freshRelayState, false);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (isFreeFlow) {
|
|
283
|
+
updateStatusBar(ctx.ui);
|
|
284
|
+
} else {
|
|
285
|
+
ctx.ui?.setStatus?.("freeflow", undefined);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
pi.on?.("session_shutdown", () => {
|
|
289
|
+
if (server) {
|
|
290
|
+
logInfo("shutting down proxy daemon...");
|
|
291
|
+
const closing = server;
|
|
292
|
+
server = null;
|
|
293
|
+
closing.close();
|
|
294
|
+
// Node 18.2+: closeIdleConnections exists at runtime even if lib types lag
|
|
295
|
+
const closable = closing as unknown as { closeIdleConnections?: () => void };
|
|
296
|
+
closable.closeIdleConnections?.();
|
|
297
|
+
resetRateLimits();
|
|
298
|
+
logInfo("shutdown complete");
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|