pi-freeflow 1.0.3 → 1.0.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.
Files changed (2) hide show
  1. package/extensions/index.ts +72 -14
  2. package/package.json +1 -1
@@ -739,22 +739,70 @@ function checkRateLimit(ip: string, upstream: Upstream): boolean {
739
739
  // Each upstream publishes a model list; fetch it ONCE (cached) and check
740
740
  // membership. A 1-token chat probe per model was too slow (large models need
741
741
  // 10s+ for the first token). Real usability is validated at chat time (300s).
742
+ const CATALOG_CACHE_FILE = path.join(
743
+ homedir(),
744
+ ".pi",
745
+ "agent",
746
+ "pi-freeflow-catalog-cache.json",
747
+ );
748
+ const CATALOG_CACHE_TTL_MS = 3600_000; // 1 hour
749
+
750
+ interface CatalogCacheData {
751
+ timestamp: number;
752
+ opencode: string[];
753
+ kilo: string[];
754
+ }
755
+
756
+ function readCatalogCache(): CatalogCacheData | null {
757
+ try {
758
+ if (!fs.existsSync(CATALOG_CACHE_FILE)) return null;
759
+ const raw = fs.readFileSync(CATALOG_CACHE_FILE, "utf8");
760
+ const data = JSON.parse(raw) as CatalogCacheData;
761
+ if (Date.now() - data.timestamp < CATALOG_CACHE_TTL_MS) {
762
+ return data;
763
+ }
764
+ } catch {}
765
+ return null;
766
+ }
767
+
768
+ function writeCatalogCache(opencode: string[], kilo: string[]): void {
769
+ try {
770
+ const dir = path.dirname(CATALOG_CACHE_FILE);
771
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
772
+ const data: CatalogCacheData = {
773
+ timestamp: Date.now(),
774
+ opencode,
775
+ kilo,
776
+ };
777
+ fs.writeFileSync(CATALOG_CACHE_FILE, JSON.stringify(data), "utf8");
778
+ } catch {}
779
+ }
780
+
742
781
  let opencodeCatalogP: Promise<Set<string> | null> | null = null;
743
782
  function opencodeCatalog(): Promise<Set<string> | null> {
744
783
  if (!opencodeCatalogP)
745
784
  opencodeCatalogP = (async () => {
785
+ const disk = readCatalogCache();
786
+ if (disk && Array.isArray(disk.opencode) && disk.opencode.length > 0) {
787
+ return new Set<string>(disk.opencode);
788
+ }
746
789
  try {
747
790
  const r = await fetch(`${API}/models`, {
748
791
  headers: opencodeHeaders(),
749
792
  signal: AbortSignal.timeout(10_000),
750
793
  });
751
- if (!r.ok) return null;
794
+ if (!r.ok) {
795
+ // Fallback to static known models if network fails
796
+ return new Set<string>(KNOWN_MODELS.map((m) => m.id));
797
+ }
752
798
  const d = await r.json();
753
- return new Set<string>(
754
- (d?.data ?? []).map((m: { id: string }) => m.id),
755
- );
799
+ const arr = (d?.data ?? []).map((m: { id: string }) => m.id);
800
+ const s = new Set<string>(arr);
801
+ const kDisk = disk?.kilo ?? KILO_MODELS.map((m) => m.id);
802
+ writeCatalogCache(arr, kDisk);
803
+ return s;
756
804
  } catch {
757
- return null;
805
+ return new Set<string>(KNOWN_MODELS.map((m) => m.id));
758
806
  }
759
807
  })();
760
808
  return opencodeCatalogP;
@@ -763,6 +811,10 @@ let kiloCatalogP: Promise<Set<string> | null> | null = null;
763
811
  function kiloCatalog(): Promise<Set<string> | null> {
764
812
  if (!kiloCatalogP)
765
813
  kiloCatalogP = (async () => {
814
+ const disk = readCatalogCache();
815
+ if (disk && Array.isArray(disk.kilo) && disk.kilo.length > 0) {
816
+ return new Set<string>(disk.kilo);
817
+ }
766
818
  try {
767
819
  const r = await fetch(
768
820
  KILO_CHAT_URL.replace("/chat/completions", "/models"),
@@ -771,13 +823,17 @@ function kiloCatalog(): Promise<Set<string> | null> {
771
823
  signal: AbortSignal.timeout(10_000),
772
824
  },
773
825
  );
774
- if (!r.ok) return null;
826
+ if (!r.ok) {
827
+ return new Set<string>(KILO_MODELS.map((m) => m.id));
828
+ }
775
829
  const d = await r.json();
776
- return new Set<string>(
777
- (d?.data ?? []).map((m: { id: string }) => m.id),
778
- );
830
+ const arr = (d?.data ?? []).map((m: { id: string }) => m.id);
831
+ const s = new Set<string>(arr);
832
+ const ocDisk = disk?.opencode ?? KNOWN_MODELS.map((m) => m.id);
833
+ writeCatalogCache(ocDisk, arr);
834
+ return s;
779
835
  } catch {
780
- return null;
836
+ return new Set<string>(KILO_MODELS.map((m) => m.id));
781
837
  }
782
838
  })();
783
839
  return kiloCatalogP;
@@ -1069,8 +1125,9 @@ function startProxy(
1069
1125
  ? Buffer.concat(bodyChunks)
1070
1126
  : undefined;
1071
1127
  if (relayBody && parsedBody) {
1072
- normalizeRequestBody(parsedBody, true);
1073
- relayBody = Buffer.from(JSON.stringify(parsedBody));
1128
+ const relayBodyObj = structuredClone(parsedBody);
1129
+ normalizeRequestBody(relayBodyObj, true);
1130
+ relayBody = Buffer.from(JSON.stringify(relayBodyObj));
1074
1131
  }
1075
1132
  const response = await relayFetch(fullUrl, {
1076
1133
  method: req.method || "POST",
@@ -1114,8 +1171,9 @@ function startProxy(
1114
1171
  // direct path (existing, untouched)
1115
1172
  let directBody = Buffer.concat(bodyChunks);
1116
1173
  if (parsedBody) {
1117
- normalizeRequestBody(parsedBody, false);
1118
- directBody = Buffer.from(JSON.stringify(parsedBody));
1174
+ const directBodyObj = structuredClone(parsedBody);
1175
+ normalizeRequestBody(directBodyObj, false);
1176
+ directBody = Buffer.from(JSON.stringify(directBodyObj));
1119
1177
  }
1120
1178
  const fwd = sanitizeHeaders(req.headers, target.hostname);
1121
1179
  if (directBody.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-freeflow",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Personal multi-cloud rolling fallback relay for OpenCode Zen and KiloCode models in OMP/Pi",
5
5
  "keywords": [
6
6
  "pi-package",