pi-freeflow 1.15.0 → 1.15.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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.15.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Requests now roll over to the next relay when the current one answers with a disabled-deployment verdict, instead of failing on the first relay and requiring a manual switch. Other payment and quota refusals still surface immediately.
8
+
9
+ ## 1.15.0
10
+
11
+ ### Minor Changes
12
+
13
+ - Free-tier requests now use native session identifiers and an up-to-date client version, after the upstream gateway began rejecting older formats with a free-tier error. `/freeflow test` also gains an end-to-end chat check that verifies a relay can actually run inference, not just list models.
14
+
3
15
  ## 1.14.0
4
16
 
5
17
  ### Minor Changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-freeflow",
3
3
  "type": "module",
4
- "version": "1.15.0",
4
+ "version": "1.15.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/relay.ts CHANGED
@@ -38,6 +38,26 @@ export function isRetriableStatus(status: number): boolean {
38
38
  (status >= 520 && status <= 530)
39
39
  );
40
40
  }
41
+
42
+ /** Vercel edge markers identifying a relay-host (not upstream) verdict. */
43
+ function hasVercelEdgeMarkers(res: Response): boolean {
44
+ return (
45
+ Boolean(res.headers.get("x-vercel-error")) ||
46
+ Boolean(res.headers.get("x-vercel-id")) ||
47
+ res.headers.get("server")?.toLowerCase().includes("vercel") === true
48
+ );
49
+ }
50
+
51
+ /**
52
+ * Gated 402 roll predicate: a 402 is a relay-host failure only when it
53
+ * carries Vercel edge markers or a DEPLOYMENT_DISABLED body match.
54
+ * Generic 402s (payment/quota) carry neither and must surface immediately.
55
+ */
56
+ function isRelayDeploymentDisabled(res: Response, bodyText: string | null): boolean {
57
+ if (res.status !== 402) return false;
58
+ if (hasVercelEdgeMarkers(res)) return true;
59
+ return bodyText !== null && bodyText.includes("DEPLOYMENT_DISABLED");
60
+ }
41
61
  /**
42
62
  * Per-conversation reasoning affinity. Callers sending caller-bound reasoning
43
63
  * pass the relay that issued it; `onServed` reports the relay that actually
@@ -230,6 +250,43 @@ export async function relayFetch(
230
250
  }
231
251
  continue;
232
252
  }
253
+ // Relay deployment disabled (402 DEPLOYMENT_DISABLED from Vercel):
254
+ // A disabled deployment is a relay-host failure, so roll to the next
255
+ // relay. A blanket 402 must NOT roll: generic 402s are payment/quota
256
+ // verdicts that must surface immediately, so gate strictly on Vercel
257
+ // edge markers or a DEPLOYMENT_DISABLED body match and otherwise fall
258
+ // through to the terminal path below. The body is peeked via a clone so
259
+ // the downstream stream stays intact; on any clone/read failure decide
260
+ // on headers alone (headerless failure reads as a generic 402).
261
+ if (res.status === 402) {
262
+ let disabledBody: string | null = null;
263
+ try {
264
+ disabledBody = (await res.clone().text()).slice(0, 8192);
265
+ } catch {
266
+ disabledBody = null;
267
+ }
268
+ if (isRelayDeploymentDisabled(res, disabledBody)) {
269
+ markRelayFailure(targetUrl, 402, "Deployment disabled (DEPLOYMENT_DISABLED) on relay host");
270
+ lastResponse?.body?.cancel().catch(() => {});
271
+ lastResponse = res;
272
+ lastResponseRelay = targetUrl;
273
+ log(
274
+ "warn",
275
+ `relay ${targetUrl} returned 402 DEPLOYMENT_DISABLED in ${elapsed}s — rolling to next relay`,
276
+ { upstream: url },
277
+ rid,
278
+ );
279
+ const now = Date.now();
280
+ if (now - lastRollNotify > ROLL_NOTIFY_MS) {
281
+ lastRollNotify = now;
282
+ const ui = getStatusUi();
283
+ if (ui?.notify) {
284
+ ui.notify(`relay ${shortRelayLabel(targetUrl)} failed (HTTP 402) — rolled to next relay`, "warning");
285
+ }
286
+ }
287
+ continue;
288
+ }
289
+ }
233
290
  if (isRetriableStatus(res.status)) {
234
291
  markRelayFailure(targetUrl, res.status);
235
292
  lastResponse?.body?.cancel().catch(() => {});