pi-freeflow 1.42.0 → 1.42.1
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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/proxy.ts +4 -1
- package/src/relay-state.ts +21 -15
- package/src/relay.ts +8 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.42.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5ab7509: Enable forced round-robin relay selection for stateless OVHcloud AI requests across all healthy pool relays, load-balancing requests to prevent hitting upstream per-IP rate limits (2 req/min) while preserving sticky session affinity for OpenCode Zen.
|
|
8
|
+
|
|
3
9
|
## 1.42.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-freeflow",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.42.
|
|
4
|
+
"version": "1.42.1",
|
|
5
5
|
"description": "Thin provider for OMP/Pi — model list + dumb relay proxy + log; host pi-ai owns thinking/normalization",
|
|
6
6
|
"main": "extensions/index.ts",
|
|
7
7
|
"types": "src/index.ts",
|
package/src/proxy.ts
CHANGED
|
@@ -787,7 +787,10 @@ delete chatBody.max_output_tokens;
|
|
|
787
787
|
signal: controller.signal,
|
|
788
788
|
},
|
|
789
789
|
reqId,
|
|
790
|
-
{
|
|
790
|
+
{
|
|
791
|
+
roundRobin: true,
|
|
792
|
+
onServed: (url) => { servedRelay = url; },
|
|
793
|
+
},
|
|
791
794
|
(failure) => audit?.attemptFailure({ attempt: failure.attempt, status: failure.status, reason: failure.reason }),
|
|
792
795
|
);
|
|
793
796
|
} else {
|
package/src/relay-state.ts
CHANGED
|
@@ -824,26 +824,32 @@ export function isSessionPinEligible(pin: string, ordered: string[]): boolean {
|
|
|
824
824
|
* a key the cursor round-robins per request. Cooling relays stay at the tail,
|
|
825
825
|
* so a rate-limited IP is never preferred.
|
|
826
826
|
*/
|
|
827
|
-
export function orderedRelayCandidates(
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
827
|
+
export function orderedRelayCandidates(
|
|
828
|
+
preferredRelay?: string,
|
|
829
|
+
spreadKey?: string,
|
|
830
|
+
forceRoundRobin?: boolean,
|
|
831
|
+
): string[] {
|
|
832
|
+
const ordered = getOrderedRelayUrls();
|
|
833
|
+
const preferred = (preferredRelay ?? "").trim();
|
|
834
|
+
if (preferred) {
|
|
835
|
+
const index = ordered.indexOf(preferred);
|
|
836
|
+
if (index <= 0) return ordered;
|
|
837
|
+
if (!isSessionPinEligible(preferred, ordered)) return ordered;
|
|
838
|
+
return [ordered[index], ...ordered.slice(0, index), ...ordered.slice(index + 1)];
|
|
839
|
+
}
|
|
840
|
+
if (activeRelayState.mode !== "spread" && !forceRoundRobin) return ordered;
|
|
841
|
+
const healthy = ordered.filter((u) => isRelayHealthy(u));
|
|
842
|
+
const cooling = ordered.filter((u) => !isRelayHealthy(u));
|
|
843
|
+
if (healthy.length <= 1) return [...healthy, ...cooling];
|
|
844
|
+
const k = spreadKey ? stableHash(spreadKey) % healthy.length : spreadCursor++ % healthy.length;
|
|
845
|
+
return [...healthy.slice(k), ...healthy.slice(0, k), ...cooling];
|
|
842
846
|
}
|
|
843
847
|
|
|
844
848
|
/** Round-robin cursor for spread mode when no stable key is available. */
|
|
845
849
|
let spreadCursor = 0;
|
|
846
850
|
|
|
851
|
+
/** Test-only: reset spread / round-robin cursor */
|
|
852
|
+
export function _resetSpreadCursorForTest(): void { spreadCursor = 0; }
|
|
847
853
|
/** FNV-1a: stable string -> bucket index for per-conversation sharding. */
|
|
848
854
|
function stableHash(s: string): number {
|
|
849
855
|
let h = 2166136261;
|
package/src/relay.ts
CHANGED
|
@@ -151,6 +151,8 @@ export interface RelayAffinity {
|
|
|
151
151
|
preferred?: string;
|
|
152
152
|
/** Stable key used to shard requests across healthy relays in spread mode. */
|
|
153
153
|
spreadKey?: string;
|
|
154
|
+
/** When true, healthy relays rotate round-robin regardless of pool mode (e.g. for OVH). */
|
|
155
|
+
roundRobin?: boolean;
|
|
154
156
|
onServed?: (relay: string | null) => void;
|
|
155
157
|
}
|
|
156
158
|
|
|
@@ -186,7 +188,7 @@ export async function relayFetch(
|
|
|
186
188
|
affinity.onServed?.(null);
|
|
187
189
|
return fetch(url, opts as unknown as RequestInit);
|
|
188
190
|
}
|
|
189
|
-
const candidates = orderedRelayCandidates(affinity.preferred, affinity.spreadKey);
|
|
191
|
+
const candidates = orderedRelayCandidates(affinity.preferred, affinity.spreadKey, affinity.roundRobin);
|
|
190
192
|
|
|
191
193
|
if (candidates.length === 0) {
|
|
192
194
|
// Empty pool: skip straight to upstream instead of logging a misleading
|
|
@@ -709,11 +711,12 @@ export async function relayFetch(
|
|
|
709
711
|
const affinityServed =
|
|
710
712
|
Boolean(affinity.preferred) &&
|
|
711
713
|
targetUrl.trim() === (affinity.preferred ?? "").trim();
|
|
712
|
-
// Spread mode
|
|
713
|
-
// healthy relay, so rewriting the sticky URL here
|
|
714
|
-
// state file and spam "active relay auto-switched" on every turn.
|
|
714
|
+
// Spread mode and forced round-robin have no single primary: every
|
|
715
|
+
// request picks its own healthy relay, so rewriting the sticky URL here
|
|
716
|
+
// would churn the state file and spam "active relay auto-switched" on every turn.
|
|
715
717
|
const spreadMode = relayState.mode === "spread";
|
|
716
|
-
|
|
718
|
+
const roundRobin = Boolean(affinity.roundRobin);
|
|
719
|
+
if (relayState.url !== targetUrl && !affinityServed && !spreadMode && !roundRobin) {
|
|
717
720
|
log("info", `active relay auto-switched to ${targetUrl}`, {
|
|
718
721
|
previous: relayState.url,
|
|
719
722
|
}, rid);
|