pi-freeflow 1.4.2 → 1.4.3
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 +325 -279
- package/package.json +1 -1
- package/src/catalog.ts +222 -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 +399 -399
- package/src/proxy.ts +503 -500
- package/src/relay.ts +213 -210
- package/src/stream-pipe.ts +265 -263
package/src/catalog.ts
CHANGED
|
@@ -1,207 +1,222 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dynamic catalog discovery, caching, and model enrichment for pi-freeflow
|
|
3
|
-
*
|
|
4
|
-
* Provides 1-hour atomic disk caching with safe offline fallback to verified static definitions.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { randomUUID } from "node:crypto";
|
|
8
|
-
import fs from "node:fs";
|
|
9
|
-
import path from "node:path";
|
|
10
|
-
import {
|
|
11
|
-
CATALOG_CACHE_FILE,
|
|
12
|
-
CATALOG_CACHE_TTL_MS,
|
|
13
|
-
KILO_CHAT_URL,
|
|
14
|
-
OPENCODE_API_URL,
|
|
15
|
-
opencodeHeaders,
|
|
16
|
-
} from "./config.ts";
|
|
17
|
-
import { log, logDebug, logWarn } from "./logger.ts";
|
|
18
|
-
import {
|
|
19
|
-
ALL_MODELS,
|
|
20
|
-
KILO_MODELS,
|
|
21
|
-
KILO_MODEL_IDS,
|
|
22
|
-
KNOWN_MODELS,
|
|
23
|
-
MODEL_MAP,
|
|
24
|
-
OPENCODE_MODELS,
|
|
25
|
-
getAllRegisteredModels,
|
|
26
|
-
} from "./models.ts";
|
|
27
|
-
import type {
|
|
28
|
-
CatalogCacheData,
|
|
29
|
-
RawModelItem,
|
|
30
|
-
RegisteredModel,
|
|
31
|
-
Upstream,
|
|
32
|
-
} from "./types.ts";
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* In-memory cache of currently active/available free models.
|
|
36
|
-
* Initialized with all 23 verified models for 0ms instant availability.
|
|
37
|
-
*/
|
|
38
|
-
let aliveCatalog: RegisteredModel[] = ALL_MODELS.map((m) => ({
|
|
39
|
-
...m,
|
|
40
|
-
source: KILO_MODEL_IDS.has(m.id) ? ("kilo" as const) : ("opencode" as const),
|
|
41
|
-
}));
|
|
42
|
-
/**
|
|
43
|
-
* Get current in-memory alive catalog
|
|
44
|
-
*/
|
|
45
|
-
export function getAliveCatalog(): RegisteredModel[] {
|
|
46
|
-
return aliveCatalog;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Set in-memory alive catalog
|
|
51
|
-
*/
|
|
52
|
-
export function setAliveCatalog(catalog: RegisteredModel[]): void {
|
|
53
|
-
aliveCatalog = catalog;
|
|
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
|
-
idLower.includes("
|
|
117
|
-
idLower.includes("
|
|
118
|
-
idLower.includes("
|
|
119
|
-
idLower.includes("
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
if (
|
|
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
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic catalog discovery, caching, and model enrichment for pi-freeflow
|
|
3
|
+
*
|
|
4
|
+
* Provides 1-hour atomic disk caching with safe offline fallback to verified static definitions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import {
|
|
11
|
+
CATALOG_CACHE_FILE,
|
|
12
|
+
CATALOG_CACHE_TTL_MS,
|
|
13
|
+
KILO_CHAT_URL,
|
|
14
|
+
OPENCODE_API_URL,
|
|
15
|
+
opencodeHeaders,
|
|
16
|
+
} from "./config.ts";
|
|
17
|
+
import { log, logDebug, logWarn } from "./logger.ts";
|
|
18
|
+
import {
|
|
19
|
+
ALL_MODELS,
|
|
20
|
+
KILO_MODELS,
|
|
21
|
+
KILO_MODEL_IDS,
|
|
22
|
+
KNOWN_MODELS,
|
|
23
|
+
MODEL_MAP,
|
|
24
|
+
OPENCODE_MODELS,
|
|
25
|
+
getAllRegisteredModels,
|
|
26
|
+
} from "./models.ts";
|
|
27
|
+
import type {
|
|
28
|
+
CatalogCacheData,
|
|
29
|
+
RawModelItem,
|
|
30
|
+
RegisteredModel,
|
|
31
|
+
Upstream,
|
|
32
|
+
} from "./types.ts";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* In-memory cache of currently active/available free models.
|
|
36
|
+
* Initialized with all 23 verified models for 0ms instant availability.
|
|
37
|
+
*/
|
|
38
|
+
let aliveCatalog: RegisteredModel[] = ALL_MODELS.map((m) => ({
|
|
39
|
+
...m,
|
|
40
|
+
source: KILO_MODEL_IDS.has(m.id) ? ("kilo" as const) : ("opencode" as const),
|
|
41
|
+
}));
|
|
42
|
+
/**
|
|
43
|
+
* Get current in-memory alive catalog
|
|
44
|
+
*/
|
|
45
|
+
export function getAliveCatalog(): RegisteredModel[] {
|
|
46
|
+
return aliveCatalog;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Set in-memory alive catalog
|
|
51
|
+
*/
|
|
52
|
+
export function setAliveCatalog(catalog: RegisteredModel[]): void {
|
|
53
|
+
aliveCatalog = catalog;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Overlay a refreshed model list onto a base list without dropping base entries.
|
|
58
|
+
* Fresh entries win on id collision; unknown fresh ids are appended after the base.
|
|
59
|
+
* Guards the background refresh against a partial upstream cache removing
|
|
60
|
+
* verified static models from provider registration.
|
|
61
|
+
*/
|
|
62
|
+
export function mergeCatalog(
|
|
63
|
+
base: RegisteredModel[],
|
|
64
|
+
fresh: RegisteredModel[],
|
|
65
|
+
): RegisteredModel[] {
|
|
66
|
+
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()];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Format a clean, human-readable display name for any upstream model ID.
|
|
73
|
+
*/
|
|
74
|
+
export function formatCleanDisplayName(id: string, customName?: string): string {
|
|
75
|
+
if (customName && customName.trim()) {
|
|
76
|
+
return customName.trim();
|
|
77
|
+
}
|
|
78
|
+
const known = MODEL_MAP.get(id);
|
|
79
|
+
if (known && known.name) {
|
|
80
|
+
return known.name;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Strip provider prefix ("nvidia/", "stepfun/", "dots-studio/", etc.)
|
|
84
|
+
let clean = id.replace(/^[a-zA-Z0-9_.-]+\//, "");
|
|
85
|
+
// Strip variant suffixes
|
|
86
|
+
clean = clean.replace(/:(free|preview|exacto|default|batch)$/i, "");
|
|
87
|
+
clean = clean.replace(/-(free|contributor|preview)$/i, "");
|
|
88
|
+
|
|
89
|
+
// Capitalize words with acronym preservation
|
|
90
|
+
const parts = clean.split(/[-_]/).map((w) => {
|
|
91
|
+
const lower = w.toLowerCase();
|
|
92
|
+
if (lower === "gpt") return "GPT";
|
|
93
|
+
if (lower === "ai") return "AI";
|
|
94
|
+
if (lower === "lfm") return "LFM";
|
|
95
|
+
if (lower === "hy3") return "Hy3";
|
|
96
|
+
if (lower === "mimo") return "MiMo";
|
|
97
|
+
if (lower === "ocr") return "OCR";
|
|
98
|
+
return w.charAt(0).toUpperCase() + w.slice(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return parts.join(" ");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Enrich a raw upstream model item into a fully typed RegisteredModel.
|
|
106
|
+
*/
|
|
107
|
+
export function enrichModelDef(raw: RawModelItem, source: Upstream): RegisteredModel {
|
|
108
|
+
const known = MODEL_MAP.get(raw.id);
|
|
109
|
+
if (known) {
|
|
110
|
+
return { ...known, source };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const idLower = raw.id.toLowerCase();
|
|
114
|
+
const hasVision =
|
|
115
|
+
idLower.includes("vision") ||
|
|
116
|
+
idLower.includes("vl") ||
|
|
117
|
+
idLower.includes("omni") ||
|
|
118
|
+
idLower.includes("note") ||
|
|
119
|
+
idLower.includes("image");
|
|
120
|
+
const hasReasoning =
|
|
121
|
+
idLower.includes("reasoning") ||
|
|
122
|
+
idLower.includes("r1") ||
|
|
123
|
+
idLower.includes("o1") ||
|
|
124
|
+
idLower.includes("think") ||
|
|
125
|
+
idLower.includes("alpha") ||
|
|
126
|
+
idLower.includes("spark");
|
|
127
|
+
|
|
128
|
+
let contextWindow =
|
|
129
|
+
typeof raw.context_length === "number" ? raw.context_length : 262_144;
|
|
130
|
+
if (
|
|
131
|
+
idLower.includes("1m") ||
|
|
132
|
+
idLower.includes("ultra") ||
|
|
133
|
+
idLower.includes("lightning") ||
|
|
134
|
+
idLower.includes("mimo-v2.5") ||
|
|
135
|
+
idLower.includes("muse-spark")
|
|
136
|
+
) {
|
|
137
|
+
contextWindow = 1_048_576;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let maxTokens =
|
|
141
|
+
typeof raw.max_output_tokens === "number"
|
|
142
|
+
? raw.max_output_tokens
|
|
143
|
+
: 65_536;
|
|
144
|
+
if (idLower.includes("ultra") || idLower.includes("lightning")) {
|
|
145
|
+
maxTokens = 131_072;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const isResponses = raw.id === "muse-spark-1.2-contributor-free";
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
id: raw.id,
|
|
152
|
+
name: formatCleanDisplayName(raw.id),
|
|
153
|
+
source,
|
|
154
|
+
reasoning: hasReasoning,
|
|
155
|
+
contextWindow,
|
|
156
|
+
maxTokens,
|
|
157
|
+
api: isResponses ? "openai-responses" : undefined,
|
|
158
|
+
input: hasVision ? ["text", "image"] : ["text"],
|
|
159
|
+
thinkingFormat: source === "kilo" && hasReasoning ? "openrouter" : undefined,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Read cached catalog data from disk if valid and unexpired.
|
|
165
|
+
*/
|
|
166
|
+
export function readCatalogCache(): CatalogCacheData | null {
|
|
167
|
+
try {
|
|
168
|
+
if (!fs.existsSync(CATALOG_CACHE_FILE)) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const raw = fs.readFileSync(CATALOG_CACHE_FILE, "utf8");
|
|
172
|
+
const data = JSON.parse(raw) as CatalogCacheData;
|
|
173
|
+
if (Date.now() - data.timestamp < CATALOG_CACHE_TTL_MS) {
|
|
174
|
+
return data;
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
logDebug("Failed reading catalog cache", { error: String(err) });
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Atomically write catalog cache data to disk using temporary file + rename.
|
|
184
|
+
*/
|
|
185
|
+
export function writeCatalogCache(data: CatalogCacheData): void {
|
|
186
|
+
try {
|
|
187
|
+
const dir = path.dirname(CATALOG_CACHE_FILE);
|
|
188
|
+
if (!fs.existsSync(dir)) {
|
|
189
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
190
|
+
}
|
|
191
|
+
const tmpPath = `${CATALOG_CACHE_FILE}.${randomUUID()}.tmp`;
|
|
192
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2), "utf8");
|
|
193
|
+
fs.renameSync(tmpPath, CATALOG_CACHE_FILE);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
logWarn("Could not persist catalog cache to disk", { error: String(err) });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Refresh free model catalog from OpenCode Zen and KiloCode Gateway endpoints.
|
|
201
|
+
* Falls back gracefully to cached or static models if network requests fail.
|
|
202
|
+
*/
|
|
203
|
+
export async function refreshCatalog(force = false): Promise<RegisteredModel[]> {
|
|
204
|
+
// Thin provider: no live fetch — subagents must not hit upstream directly
|
|
205
|
+
// (proxy-only). Host Pi/OMP owns dynamic discovery via fetchDynamicModels (24h).
|
|
206
|
+
// We only serve disk cache if fresh, otherwise static 23-model aliveCatalog.
|
|
207
|
+
const disk = readCatalogCache();
|
|
208
|
+
if (disk && Array.isArray(disk.models) && disk.models.length > 0) {
|
|
209
|
+
const age = Date.now() - (disk.timestamp ?? 0);
|
|
210
|
+
if (!force && age < CATALOG_CACHE_TTL_MS) {
|
|
211
|
+
aliveCatalog = disk.models;
|
|
212
|
+
return aliveCatalog;
|
|
213
|
+
}
|
|
214
|
+
// Stale cache still better than empty — return it without network
|
|
215
|
+
if (disk.models.length >= 23) {
|
|
216
|
+
aliveCatalog = disk.models;
|
|
217
|
+
return aliveCatalog;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// No valid cache — return in-memory static 23 (host will refresh if needed)
|
|
221
|
+
return aliveCatalog;
|
|
222
|
+
}
|