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 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.0",
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
- { onServed: (url) => { servedRelay = url; } },
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 {
@@ -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(preferredRelay?: string, spreadKey?: string): string[] {
828
- const ordered = getOrderedRelayUrls();
829
- const preferred = (preferredRelay ?? "").trim();
830
- if (preferred) {
831
- const index = ordered.indexOf(preferred);
832
- if (index <= 0) return ordered;
833
- if (!isSessionPinEligible(preferred, ordered)) return ordered;
834
- return [ordered[index], ...ordered.slice(0, index), ...ordered.slice(index + 1)];
835
- }
836
- if (activeRelayState.mode !== "spread") return ordered;
837
- const healthy = ordered.filter((u) => isRelayHealthy(u));
838
- const cooling = ordered.filter((u) => !isRelayHealthy(u));
839
- if (healthy.length <= 1) return [...healthy, ...cooling];
840
- const k = spreadKey ? stableHash(spreadKey) % healthy.length : spreadCursor++ % healthy.length;
841
- return [...healthy.slice(k), ...healthy.slice(0, k), ...cooling];
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 has no single primary: every request picks its own
713
- // healthy relay, so rewriting the sticky URL here would churn the
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
- if (relayState.url !== targetUrl && !affinityServed && !spreadMode) {
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);