pi-freeflow 1.1.3 → 1.1.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/extensions/index.ts +164 -112
- package/package.json +1 -1
package/extensions/index.ts
CHANGED
|
@@ -746,10 +746,7 @@ function checkRateLimit(ip: string, upstream: Upstream): boolean {
|
|
|
746
746
|
return true;
|
|
747
747
|
}
|
|
748
748
|
|
|
749
|
-
// ── Health Check
|
|
750
|
-
// Each upstream publishes a model list; fetch it ONCE (cached) and check
|
|
751
|
-
// membership. A 1-token chat probe per model was too slow (large models need
|
|
752
|
-
// 10s+ for the first token). Real usability is validated at chat time (300s).
|
|
749
|
+
// ── Health Check & Dynamic Catalog Auto-Update ────────────────────
|
|
753
750
|
const CATALOG_CACHE_FILE = path.join(
|
|
754
751
|
homedir(),
|
|
755
752
|
".pi",
|
|
@@ -758,10 +755,98 @@ const CATALOG_CACHE_FILE = path.join(
|
|
|
758
755
|
);
|
|
759
756
|
const CATALOG_CACHE_TTL_MS = 3600_000; // 1 hour
|
|
760
757
|
|
|
758
|
+
function formatCleanDisplayName(id: string, customName?: string): string {
|
|
759
|
+
if (customName && customName.trim()) return customName.trim();
|
|
760
|
+
const known = MODEL_MAP.get(id);
|
|
761
|
+
if (known && known.name) return known.name;
|
|
762
|
+
|
|
763
|
+
// Strip provider prefix ("nvidia/", "stepfun/", "dots-studio/", etc.)
|
|
764
|
+
let clean = id.replace(/^[a-zA-Z0-9_.-]+\//, "");
|
|
765
|
+
// Strip variant suffixes
|
|
766
|
+
clean = clean.replace(/:(free|preview|exacto|default|batch)$/i, "");
|
|
767
|
+
clean = clean.replace(/-(free|contributor|preview)$/i, "");
|
|
768
|
+
|
|
769
|
+
// Capitalize words nicely
|
|
770
|
+
const parts = clean.split(/[-_]/).map((w) => {
|
|
771
|
+
const lower = w.toLowerCase();
|
|
772
|
+
if (lower === "gpt") return "GPT";
|
|
773
|
+
if (lower === "ai") return "AI";
|
|
774
|
+
if (lower === "lfm") return "LFM";
|
|
775
|
+
if (lower === "hy3") return "Hy3";
|
|
776
|
+
if (lower === "mimo") return "MiMo";
|
|
777
|
+
if (lower === "ocr") return "OCR";
|
|
778
|
+
return w.charAt(0).toUpperCase() + w.slice(1);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
return parts.join(" ");
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
interface RawModelItem {
|
|
785
|
+
id: string;
|
|
786
|
+
context_length?: number;
|
|
787
|
+
max_output_tokens?: number;
|
|
788
|
+
[key: string]: unknown;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function enrichModelDef(raw: RawModelItem, source: Upstream): RegisteredModel {
|
|
792
|
+
const known = MODEL_MAP.get(raw.id);
|
|
793
|
+
if (known) return { ...known, source };
|
|
794
|
+
|
|
795
|
+
const idLower = raw.id.toLowerCase();
|
|
796
|
+
const hasVision =
|
|
797
|
+
idLower.includes("vision") ||
|
|
798
|
+
idLower.includes("vl") ||
|
|
799
|
+
idLower.includes("omni") ||
|
|
800
|
+
idLower.includes("note") ||
|
|
801
|
+
idLower.includes("image");
|
|
802
|
+
const hasReasoning =
|
|
803
|
+
idLower.includes("reasoning") ||
|
|
804
|
+
idLower.includes("r1") ||
|
|
805
|
+
idLower.includes("o1") ||
|
|
806
|
+
idLower.includes("think") ||
|
|
807
|
+
idLower.includes("alpha") ||
|
|
808
|
+
idLower.includes("spark");
|
|
809
|
+
|
|
810
|
+
let contextWindow =
|
|
811
|
+
typeof raw.context_length === "number" ? raw.context_length : 262_144;
|
|
812
|
+
if (
|
|
813
|
+
idLower.includes("1m") ||
|
|
814
|
+
idLower.includes("ultra") ||
|
|
815
|
+
idLower.includes("lightning") ||
|
|
816
|
+
idLower.includes("mimo-v2.5") ||
|
|
817
|
+
idLower.includes("muse-spark")
|
|
818
|
+
) {
|
|
819
|
+
contextWindow = 1_048_576;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
let maxTokens =
|
|
823
|
+
typeof raw.max_output_tokens === "number"
|
|
824
|
+
? raw.max_output_tokens
|
|
825
|
+
: 65_536;
|
|
826
|
+
if (idLower.includes("ultra") || idLower.includes("lightning")) {
|
|
827
|
+
maxTokens = 131_072;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const isResponses = raw.id === "muse-spark-1.2-contributor-free";
|
|
831
|
+
|
|
832
|
+
return {
|
|
833
|
+
id: raw.id,
|
|
834
|
+
name: formatCleanDisplayName(raw.id),
|
|
835
|
+
source,
|
|
836
|
+
reasoning: hasReasoning,
|
|
837
|
+
contextWindow,
|
|
838
|
+
maxTokens,
|
|
839
|
+
api: isResponses ? "openai-responses" : undefined,
|
|
840
|
+
input: hasVision ? ["text", "image"] : ["text"],
|
|
841
|
+
thinkingFormat: source === "kilo" && hasReasoning ? "openrouter" : undefined,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
761
845
|
interface CatalogCacheData {
|
|
762
846
|
timestamp: number;
|
|
763
847
|
opencode: string[];
|
|
764
848
|
kilo: string[];
|
|
849
|
+
models?: RegisteredModel[];
|
|
765
850
|
}
|
|
766
851
|
|
|
767
852
|
function readCatalogCache(): CatalogCacheData | null {
|
|
@@ -776,99 +861,81 @@ function readCatalogCache(): CatalogCacheData | null {
|
|
|
776
861
|
return null;
|
|
777
862
|
}
|
|
778
863
|
|
|
779
|
-
function
|
|
864
|
+
async function refreshCatalog(force = false): Promise<RegisteredModel[]> {
|
|
865
|
+
if (!force) {
|
|
866
|
+
const disk = readCatalogCache();
|
|
867
|
+
if (disk && Array.isArray(disk.models) && disk.models.length > 0) {
|
|
868
|
+
aliveCatalog = disk.models;
|
|
869
|
+
return aliveCatalog;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// 1. Fetch OpenCode Zen models
|
|
874
|
+
let opencodeList: RegisteredModel[] = [];
|
|
875
|
+
try {
|
|
876
|
+
const r = await fetch(`${API}/models`, {
|
|
877
|
+
headers: opencodeHeaders(),
|
|
878
|
+
signal: AbortSignal.timeout(10_000),
|
|
879
|
+
});
|
|
880
|
+
if (r.ok) {
|
|
881
|
+
const d = await r.json();
|
|
882
|
+
const items: RawModelItem[] = Array.isArray(d?.data) ? d.data : [];
|
|
883
|
+
const aliveIds = new Set(items.map((m) => m.id));
|
|
884
|
+
opencodeList = KNOWN_MODELS.filter((m) => aliveIds.has(m.id)).map((m) => ({
|
|
885
|
+
...m,
|
|
886
|
+
source: "opencode" as const,
|
|
887
|
+
}));
|
|
888
|
+
}
|
|
889
|
+
} catch {}
|
|
890
|
+
if (!opencodeList.length) {
|
|
891
|
+
opencodeList = KNOWN_MODELS.map((m) => ({ ...m, source: "opencode" as const }));
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// 2. Fetch KiloCode Gateway models
|
|
895
|
+
let kiloList: RegisteredModel[] = [];
|
|
896
|
+
try {
|
|
897
|
+
const r = await fetch(
|
|
898
|
+
KILO_CHAT_URL.replace("/chat/completions", "/models"),
|
|
899
|
+
{
|
|
900
|
+
headers: { Authorization: "Bearer kilo-free" },
|
|
901
|
+
signal: AbortSignal.timeout(10_000),
|
|
902
|
+
},
|
|
903
|
+
);
|
|
904
|
+
if (r.ok) {
|
|
905
|
+
const d = await r.json();
|
|
906
|
+
const items: RawModelItem[] = Array.isArray(d?.data) ? d.data : [];
|
|
907
|
+
const aliveIds = new Set(items.map((m) => m.id));
|
|
908
|
+
kiloList = KILO_MODELS.filter((m) => aliveIds.has(m.id)).map((m) => ({
|
|
909
|
+
...m,
|
|
910
|
+
source: "kilo" as const,
|
|
911
|
+
}));
|
|
912
|
+
}
|
|
913
|
+
} catch {}
|
|
914
|
+
if (!kiloList.length) {
|
|
915
|
+
kiloList = KILO_MODELS.map((m) => ({ ...m, source: "kilo" as const }));
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const all = [...opencodeList, ...kiloList];
|
|
919
|
+
aliveCatalog = all;
|
|
920
|
+
|
|
921
|
+
// Write rich models to cache atomically
|
|
780
922
|
try {
|
|
781
923
|
const dir = path.dirname(CATALOG_CACHE_FILE);
|
|
782
924
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
783
925
|
const data: CatalogCacheData = {
|
|
784
926
|
timestamp: Date.now(),
|
|
785
|
-
opencode,
|
|
786
|
-
kilo,
|
|
927
|
+
opencode: opencodeList.map((m) => m.id),
|
|
928
|
+
kilo: kiloList.map((m) => m.id),
|
|
929
|
+
models: all,
|
|
787
930
|
};
|
|
788
931
|
const tmpPath = `${CATALOG_CACHE_FILE}.${randomUUID()}.tmp`;
|
|
789
|
-
fs.writeFileSync(tmpPath, JSON.stringify(data), "utf8");
|
|
932
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2), "utf8");
|
|
790
933
|
fs.renameSync(tmpPath, CATALOG_CACHE_FILE);
|
|
791
934
|
} catch {}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
let opencodeCatalogP: Promise<Set<string> | null> | null = null;
|
|
795
|
-
function opencodeCatalog(): Promise<Set<string> | null> {
|
|
796
|
-
if (!opencodeCatalogP)
|
|
797
|
-
opencodeCatalogP = (async () => {
|
|
798
|
-
const disk = readCatalogCache();
|
|
799
|
-
if (disk && Array.isArray(disk.opencode) && disk.opencode.length > 0) {
|
|
800
|
-
return new Set<string>(disk.opencode);
|
|
801
|
-
}
|
|
802
|
-
try {
|
|
803
|
-
const r = await fetch(`${API}/models`, {
|
|
804
|
-
headers: opencodeHeaders(),
|
|
805
|
-
signal: AbortSignal.timeout(10_000),
|
|
806
|
-
});
|
|
807
|
-
if (!r.ok) {
|
|
808
|
-
// Fallback to static known models if network fails
|
|
809
|
-
return new Set<string>(KNOWN_MODELS.map((m) => m.id));
|
|
810
|
-
}
|
|
811
|
-
const d = await r.json();
|
|
812
|
-
const arr = (d?.data ?? []).map((m: { id: string }) => m.id);
|
|
813
|
-
const s = new Set<string>(arr);
|
|
814
|
-
const kDisk = disk?.kilo ?? KILO_MODELS.map((m) => m.id);
|
|
815
|
-
writeCatalogCache(arr, kDisk);
|
|
816
|
-
return s;
|
|
817
|
-
} catch {
|
|
818
|
-
return new Set<string>(KNOWN_MODELS.map((m) => m.id));
|
|
819
|
-
}
|
|
820
|
-
})();
|
|
821
|
-
return opencodeCatalogP;
|
|
822
|
-
}
|
|
823
|
-
let kiloCatalogP: Promise<Set<string> | null> | null = null;
|
|
824
|
-
function kiloCatalog(): Promise<Set<string> | null> {
|
|
825
|
-
if (!kiloCatalogP)
|
|
826
|
-
kiloCatalogP = (async () => {
|
|
827
|
-
const disk = readCatalogCache();
|
|
828
|
-
if (disk && Array.isArray(disk.kilo) && disk.kilo.length > 0) {
|
|
829
|
-
return new Set<string>(disk.kilo);
|
|
830
|
-
}
|
|
831
|
-
try {
|
|
832
|
-
const r = await fetch(
|
|
833
|
-
KILO_CHAT_URL.replace("/chat/completions", "/models"),
|
|
834
|
-
{
|
|
835
|
-
headers: { Authorization: "Bearer kilo-free" },
|
|
836
|
-
signal: AbortSignal.timeout(10_000),
|
|
837
|
-
},
|
|
838
|
-
);
|
|
839
|
-
if (!r.ok) {
|
|
840
|
-
return new Set<string>(KILO_MODELS.map((m) => m.id));
|
|
841
|
-
}
|
|
842
|
-
const d = await r.json();
|
|
843
|
-
const arr = (d?.data ?? []).map((m: { id: string }) => m.id);
|
|
844
|
-
const s = new Set<string>(arr);
|
|
845
|
-
const ocDisk = disk?.opencode ?? KNOWN_MODELS.map((m) => m.id);
|
|
846
|
-
writeCatalogCache(ocDisk, arr);
|
|
847
|
-
return s;
|
|
848
|
-
} catch {
|
|
849
|
-
return new Set<string>(KILO_MODELS.map((m) => m.id));
|
|
850
|
-
}
|
|
851
|
-
})();
|
|
852
|
-
return kiloCatalogP;
|
|
853
|
-
}
|
|
854
935
|
|
|
855
|
-
|
|
856
|
-
try {
|
|
857
|
-
const cat = await opencodeCatalog();
|
|
858
|
-
return cat ? cat.has(id) : false;
|
|
859
|
-
} catch {
|
|
860
|
-
return false;
|
|
861
|
-
}
|
|
936
|
+
return all;
|
|
862
937
|
}
|
|
863
938
|
|
|
864
|
-
async function checkKiloAlive(id: string): Promise<boolean> {
|
|
865
|
-
try {
|
|
866
|
-
const cat = await kiloCatalog();
|
|
867
|
-
return cat ? cat.has(id) : false;
|
|
868
|
-
} catch {
|
|
869
|
-
return false;
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
939
|
|
|
873
940
|
// ── Helpers ────────────────────────────────────────────────────────
|
|
874
941
|
function getClientIP(req: http.IncomingMessage): string {
|
|
@@ -1371,31 +1438,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1371
1438
|
return;
|
|
1372
1439
|
}
|
|
1373
1440
|
}
|
|
1374
|
-
|
|
1375
|
-
log("info", `checking ${KNOWN_MODELS.length} opencode model(s)...`);
|
|
1376
|
-
const opencodeChecks = await Promise.all(
|
|
1377
|
-
KNOWN_MODELS.map(async (model) => {
|
|
1378
|
-
const alive = await checkModelAlive(model.id);
|
|
1379
|
-
if (alive) log("info", `✓ ${model.id} is alive`);
|
|
1380
|
-
else log("warn", `✗ ${model.id} is dead — skipping`);
|
|
1381
|
-
return { ...model, alive, source: "opencode" as const };
|
|
1382
|
-
}),
|
|
1383
|
-
);
|
|
1384
|
-
|
|
1385
|
-
// Health check kilo models
|
|
1386
|
-
log("info", `checking ${KILO_MODELS.length} kilo model(s)...`);
|
|
1387
|
-
const kiloChecks = await Promise.all(
|
|
1388
|
-
KILO_MODELS.map(async (model) => {
|
|
1389
|
-
const alive = await checkKiloAlive(model.id);
|
|
1390
|
-
if (alive) log("info", `✓ ${model.id} (kilo) is alive`);
|
|
1391
|
-
else log("warn", `✗ ${model.id} (kilo) is dead — skipping`);
|
|
1392
|
-
return { ...model, alive, source: "kilo" as const };
|
|
1393
|
-
}),
|
|
1394
|
-
);
|
|
1395
|
-
|
|
1396
|
-
const aliveModels = [...opencodeChecks, ...kiloChecks].filter((m) => m.alive);
|
|
1441
|
+
const aliveModels = await refreshCatalog();
|
|
1397
1442
|
aliveCatalog = aliveModels;
|
|
1398
|
-
|
|
1399
1443
|
if (aliveModels.length === 0) {
|
|
1400
1444
|
// Don't bail: still register /bansos below so the user can recover
|
|
1401
1445
|
// (e.g. switch the relay off) instead of being stranded with no command.
|
|
@@ -1439,9 +1483,9 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1439
1483
|
// ── /bansos command: toggle relay egress live (on|off|status|url [URL]) ───
|
|
1440
1484
|
const commandSpec = {
|
|
1441
1485
|
description:
|
|
1442
|
-
"Relay egress: on | off | status | logs | url [URL] | deploy | list | use <URL> | remove <URL>",
|
|
1486
|
+
"Relay egress: on | off | status | logs | url [URL] | deploy | list | use <URL> | remove <URL> | refresh",
|
|
1443
1487
|
getArgumentCompletions: (prefix: string) =>
|
|
1444
|
-
["on", "off", "status", "url", "deploy", "list", "use", "remove"]
|
|
1488
|
+
["on", "off", "status", "url", "deploy", "list", "use", "remove", "refresh", "models"]
|
|
1445
1489
|
.filter((s) => s.startsWith(prefix))
|
|
1446
1490
|
.map((s) => ({ value: s, label: s })),
|
|
1447
1491
|
handler: async (args: string, ctx) => {
|
|
@@ -1582,7 +1626,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1582
1626
|
} else if (sub === "logs" || sub === "log") {
|
|
1583
1627
|
try {
|
|
1584
1628
|
if (!fs.existsSync(LOG_FILE)) {
|
|
1585
|
-
ctx.ui.notify(
|
|
1629
|
+
ctx.ui.notify("Log file is empty", "info");
|
|
1586
1630
|
return;
|
|
1587
1631
|
}
|
|
1588
1632
|
const content = fs.readFileSync(LOG_FILE, "utf8");
|
|
@@ -1591,6 +1635,14 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1591
1635
|
} catch (e) {
|
|
1592
1636
|
ctx.ui.notify(`Could not read log file: ${(e as Error).message}`, "error");
|
|
1593
1637
|
}
|
|
1638
|
+
} else if (sub === "refresh" || sub === "reload" || sub === "models") {
|
|
1639
|
+
ctx.ui.notify("Refreshing model catalog from live upstreams…", "info");
|
|
1640
|
+
const updated = await refreshCatalog(true);
|
|
1641
|
+
persist();
|
|
1642
|
+
ctx.ui.notify(
|
|
1643
|
+
`✓ Refreshed ${updated.length} models with full-spec metadata!`,
|
|
1644
|
+
"info",
|
|
1645
|
+
);
|
|
1594
1646
|
} else if (sub === "remove") {
|
|
1595
1647
|
const url = (
|
|
1596
1648
|
rest ||
|