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.
- package/extensions/index.ts +121 -38
- package/package.json +1 -1
package/extensions/index.ts
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
754
|
-
|
|
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
|
|
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)
|
|
826
|
+
if (!r.ok) {
|
|
827
|
+
return new Set<string>(KILO_MODELS.map((m) => m.id));
|
|
828
|
+
}
|
|
775
829
|
const d = await r.json();
|
|
776
|
-
|
|
777
|
-
|
|
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
|
|
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
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
|
|
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) {
|