pi-freeflow 1.0.2 → 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 +121 -38
  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;
@@ -844,6 +900,54 @@ function sanitizeHeaders(
844
900
  return sanitized;
845
901
  }
846
902
 
903
+ function normalizeRequestBody(
904
+ body: Record<string, unknown>,
905
+ isRelay = false,
906
+ ): Record<string, unknown> {
907
+ // 1. Tool choice normalization (OpenCode Zen only supports "auto" or undefined)
908
+ if (body.tool_choice === "none") {
909
+ delete body.tool_choice;
910
+ delete body.tools;
911
+ } else if (body.tool_choice && body.tool_choice !== "auto") {
912
+ body.tool_choice = "auto";
913
+ }
914
+
915
+ // 2. Reasoning normalization
916
+ if (typeof body.reasoning_effort === "string") {
917
+ const re = body.reasoning_effort.toLowerCase();
918
+ if (re === "xhigh" || re === "max") {
919
+ body.reasoning_effort = "max";
920
+ } else if (re === "high" || re === "medium") {
921
+ body.reasoning_effort = "high";
922
+ } else if (re === "none" || re === "off") {
923
+ delete body.reasoning_effort;
924
+ } else {
925
+ body.reasoning_effort = "low";
926
+ }
927
+ }
928
+ if (body.reasoning && typeof body.reasoning === "object") {
929
+ const r = body.reasoning as Record<string, unknown>;
930
+ if (r.effort === "none" || r.effort === "off") {
931
+ delete r.effort;
932
+ } else if (typeof r.effort === "string") {
933
+ const re = r.effort.toLowerCase();
934
+ if (re === "xhigh" || re === "max") r.effort = "max";
935
+ else if (re === "high" || re === "medium") r.effort = "high";
936
+ else r.effort = "low";
937
+ }
938
+ }
939
+
940
+ // 3. Max tokens clamp for Vercel relay
941
+ if (isRelay) {
942
+ const mt = body.max_tokens ?? body.maxTokens;
943
+ if (typeof mt === "number" && mt > RELAY_MAX_TOKENS) {
944
+ body.max_tokens = RELAY_MAX_TOKENS;
945
+ }
946
+ }
947
+
948
+ return body;
949
+ }
950
+
847
951
  // ponytail: shared stream pipe — upstream abort/timeout must end response,
848
952
  // not become an uncaught exception that crashes pi.
849
953
  function pipeUpstreamStream(
@@ -1017,28 +1121,13 @@ function startProxy(
1017
1121
  activeHost,
1018
1122
  );
1019
1123
  try {
1020
- // ponytail: clamp max_tokens for Vercel relay — large values
1021
- // cause 400 "Upstream request failed" (response size / duration
1022
- // limits). Direct mode stays unconstrained.
1023
1124
  let relayBody = bodyChunks.length
1024
1125
  ? Buffer.concat(bodyChunks)
1025
1126
  : undefined;
1026
1127
  if (relayBody && parsedBody) {
1027
- const mt = parsedBody.max_tokens ?? parsedBody.maxTokens;
1028
- if (typeof mt === "number" && mt > RELAY_MAX_TOKENS) {
1029
- parsedBody.max_tokens = RELAY_MAX_TOKENS;
1030
- }
1031
- if (typeof parsedBody.reasoning_effort === "string") {
1032
- const re = parsedBody.reasoning_effort.toLowerCase();
1033
- if (re === "xhigh" || re === "max") {
1034
- parsedBody.reasoning_effort = "max";
1035
- } else if (re === "high" || re === "medium") {
1036
- parsedBody.reasoning_effort = "high";
1037
- } else {
1038
- parsedBody.reasoning_effort = "low";
1039
- }
1040
- }
1041
- relayBody = Buffer.from(JSON.stringify(parsedBody));
1128
+ const relayBodyObj = structuredClone(parsedBody);
1129
+ normalizeRequestBody(relayBodyObj, true);
1130
+ relayBody = Buffer.from(JSON.stringify(relayBodyObj));
1042
1131
  }
1043
1132
  const response = await relayFetch(fullUrl, {
1044
1133
  method: req.method || "POST",
@@ -1081,16 +1170,10 @@ function startProxy(
1081
1170
  }
1082
1171
  // direct path (existing, untouched)
1083
1172
  let directBody = Buffer.concat(bodyChunks);
1084
- if (parsedBody && typeof parsedBody.reasoning_effort === "string") {
1085
- const re = parsedBody.reasoning_effort.toLowerCase();
1086
- if (re === "xhigh" || re === "max") {
1087
- parsedBody.reasoning_effort = "max";
1088
- } else if (re === "high" || re === "medium") {
1089
- parsedBody.reasoning_effort = "high";
1090
- } else {
1091
- parsedBody.reasoning_effort = "low";
1092
- }
1093
- directBody = Buffer.from(JSON.stringify(parsedBody));
1173
+ if (parsedBody) {
1174
+ const directBodyObj = structuredClone(parsedBody);
1175
+ normalizeRequestBody(directBodyObj, false);
1176
+ directBody = Buffer.from(JSON.stringify(directBodyObj));
1094
1177
  }
1095
1178
  const fwd = sanitizeHeaders(req.headers, target.hostname);
1096
1179
  if (directBody.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-freeflow",
3
- "version": "1.0.2",
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",