pi-freeflow 1.40.1 → 1.41.0
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 +55 -0
- package/README.md +36 -19
- package/package.json +1 -1
- package/src/catalog.ts +33 -21
- package/src/cline-accounts.ts +4 -3
- package/src/config.ts +13 -0
- package/src/models.ts +240 -64
- package/src/opencode-fingerprint.ts +78 -17
- package/src/proxy.ts +339 -12
- package/src/relay.ts +38 -0
- package/src/responses.ts +6 -0
- package/src/stream-pipe.ts +391 -67
- package/src/tool-translation.ts +15 -12
- package/src/toolcall-audit.ts +580 -0
package/src/relay.ts
CHANGED
|
@@ -165,12 +165,18 @@ export interface RelayAffinity {
|
|
|
165
165
|
* and `onServed` reports which relay actually produced the response —
|
|
166
166
|
* `null` for the direct fallback — so the caller does not have to infer
|
|
167
167
|
* the issuer from mutable global state.
|
|
168
|
+
* @param onAttemptFailure Observation hook: called once for every attempt this
|
|
169
|
+
* turn abandons in favour of another (each roll branch, plus the header
|
|
170
|
+
* deadline and network faults). `attempt` is 1-based and monotonic across
|
|
171
|
+
* candidates and hedges; `status` is the HTTP status when headers arrived.
|
|
172
|
+
* A throwing hook is swallowed — observation must never break a roll.
|
|
168
173
|
*/
|
|
169
174
|
export async function relayFetch(
|
|
170
175
|
url: string,
|
|
171
176
|
opts: RequestInit = {},
|
|
172
177
|
reqId?: string,
|
|
173
178
|
affinity: RelayAffinity = {},
|
|
179
|
+
onAttemptFailure?: (info: { attempt: number; status?: number; reason: string }) => void,
|
|
174
180
|
): Promise<Response> {
|
|
175
181
|
const rid = reqId || randomUUID().slice(0, 8);
|
|
176
182
|
const relayState = getActiveRelayState();
|
|
@@ -200,6 +206,23 @@ export async function relayFetch(
|
|
|
200
206
|
let sawBare500 = false;
|
|
201
207
|
/** True only while every candidate that produced a response answered 429. */
|
|
202
208
|
let rateLimitOnly = true;
|
|
209
|
+
/**
|
|
210
|
+
* Abandoned candidates in roll order, 1-based — the numbering the failure
|
|
211
|
+
* hook reports. A candidate rejected before any fetch consumes a number
|
|
212
|
+
* too: the roll abandoned it just the same.
|
|
213
|
+
*/
|
|
214
|
+
let attemptNo = 0;
|
|
215
|
+
const reportAbandoned = (reason: string, status?: number): void => {
|
|
216
|
+
attemptNo += 1;
|
|
217
|
+
if (!onAttemptFailure) return;
|
|
218
|
+
try {
|
|
219
|
+
onAttemptFailure(status === undefined
|
|
220
|
+
? { attempt: attemptNo, reason }
|
|
221
|
+
: { attempt: attemptNo, status, reason });
|
|
222
|
+
} catch {
|
|
223
|
+
// Observation is best-effort; never let it disturb the roll.
|
|
224
|
+
}
|
|
225
|
+
};
|
|
203
226
|
const u = new URL(url);
|
|
204
227
|
const relayTarget = `${u.protocol}//${u.host}`;
|
|
205
228
|
const relayPath = `${u.pathname}${u.search}`;
|
|
@@ -248,6 +271,7 @@ export async function relayFetch(
|
|
|
248
271
|
{ upstream: url },
|
|
249
272
|
rid,
|
|
250
273
|
);
|
|
274
|
+
reportAbandoned("invalid-relay-url");
|
|
251
275
|
continue;
|
|
252
276
|
}
|
|
253
277
|
// Per-candidate wiring: relay-target headers, per-relay auth, and a
|
|
@@ -398,6 +422,11 @@ export async function relayFetch(
|
|
|
398
422
|
throw hedgeErr;
|
|
399
423
|
}
|
|
400
424
|
markRelayFailure(nextUrl, 0, (hedgeErr as Error)?.message || String(hedgeErr));
|
|
425
|
+
// Both legs of this race were abandoned; the outer catch
|
|
426
|
+
// reports the primary one, this reports the hedge leg.
|
|
427
|
+
reportAbandoned(
|
|
428
|
+
(hedgeErr as Error)?.name === "AbortError" ? "header-timeout" : "network-error",
|
|
429
|
+
);
|
|
401
430
|
hedgeSkipNext = true;
|
|
402
431
|
throw hedgeErr;
|
|
403
432
|
}
|
|
@@ -425,6 +454,7 @@ export async function relayFetch(
|
|
|
425
454
|
ui.notify(`relay ${shortRelayLabel(targetUrl)} hit HTTP 504 — rolled to next relay`, "warning");
|
|
426
455
|
}
|
|
427
456
|
}
|
|
457
|
+
reportAbandoned("gateway-timeout", 504);
|
|
428
458
|
continue;
|
|
429
459
|
}
|
|
430
460
|
// Relay payload cap hit (413: request exceeds host payload limit):
|
|
@@ -449,6 +479,7 @@ export async function relayFetch(
|
|
|
449
479
|
ui.notify(`relay ${shortRelayLabel(targetUrl)} hit payload limit — trying next path`, "warning");
|
|
450
480
|
}
|
|
451
481
|
}
|
|
482
|
+
reportAbandoned("payload-too-large", 413);
|
|
452
483
|
continue;
|
|
453
484
|
}
|
|
454
485
|
|
|
@@ -484,6 +515,7 @@ export async function relayFetch(
|
|
|
484
515
|
ui.notify(`relay ${shortRelayLabel(targetUrl)} failed (HTTP 404) — rolled to next relay`, "warning");
|
|
485
516
|
}
|
|
486
517
|
}
|
|
518
|
+
reportAbandoned("relay-edge-404", 404);
|
|
487
519
|
continue;
|
|
488
520
|
}
|
|
489
521
|
// Relay deployment disabled (402 DEPLOYMENT_DISABLED from Vercel):
|
|
@@ -522,6 +554,7 @@ export async function relayFetch(
|
|
|
522
554
|
ui.notify(`relay ${shortRelayLabel(targetUrl)} failed (HTTP 402) — rolled to next relay`, "warning");
|
|
523
555
|
}
|
|
524
556
|
}
|
|
557
|
+
reportAbandoned("deployment-disabled", 402);
|
|
525
558
|
continue;
|
|
526
559
|
}
|
|
527
560
|
}
|
|
@@ -544,6 +577,7 @@ export async function relayFetch(
|
|
|
544
577
|
{ upstream: url, status: 429 },
|
|
545
578
|
rid,
|
|
546
579
|
);
|
|
580
|
+
reportAbandoned("rate-limited", 429);
|
|
547
581
|
continue;
|
|
548
582
|
}
|
|
549
583
|
log(
|
|
@@ -561,6 +595,7 @@ export async function relayFetch(
|
|
|
561
595
|
ui.notify(`relay ${shortRelayLabel(targetUrl)} failed (HTTP ${res.status}) — rolled to next relay`, "warning");
|
|
562
596
|
}
|
|
563
597
|
}
|
|
598
|
+
reportAbandoned("retriable-status", res.status);
|
|
564
599
|
continue;
|
|
565
600
|
}
|
|
566
601
|
|
|
@@ -580,6 +615,7 @@ export async function relayFetch(
|
|
|
580
615
|
{ upstream: url, status: 500 },
|
|
581
616
|
rid,
|
|
582
617
|
);
|
|
618
|
+
reportAbandoned("server-error", 500);
|
|
583
619
|
continue;
|
|
584
620
|
}
|
|
585
621
|
// Second 500 of the turn: the cap is spent and this is the
|
|
@@ -652,6 +688,7 @@ export async function relayFetch(
|
|
|
652
688
|
);
|
|
653
689
|
}
|
|
654
690
|
}
|
|
691
|
+
reportAbandoned(authRejected ? "relay-auth-rejected" : "relay-forbidden", res.status);
|
|
655
692
|
continue;
|
|
656
693
|
}
|
|
657
694
|
// SUCCESS or non-retriable client error (e.g. 200, 404):
|
|
@@ -723,6 +760,7 @@ export async function relayFetch(
|
|
|
723
760
|
{ upstream: url, error: errMsg },
|
|
724
761
|
rid,
|
|
725
762
|
);
|
|
763
|
+
reportAbandoned((err as Error)?.name === "AbortError" ? "header-timeout" : "network-error");
|
|
726
764
|
continue;
|
|
727
765
|
}
|
|
728
766
|
}
|
package/src/responses.ts
CHANGED
|
@@ -297,6 +297,12 @@ export function prepareChatFailoverBody(parsedBody: Record<string, unknown>): vo
|
|
|
297
297
|
if (typeof parsedBody.max_tokens === "number" && parsedBody.max_tokens > KILO_FAILOVER_MAX_OUTPUT_TOKENS) {
|
|
298
298
|
parsedBody.max_tokens = KILO_FAILOVER_MAX_OUTPUT_TOKENS;
|
|
299
299
|
}
|
|
300
|
+
if (
|
|
301
|
+
typeof parsedBody.max_completion_tokens === "number" &&
|
|
302
|
+
parsedBody.max_completion_tokens > KILO_FAILOVER_MAX_OUTPUT_TOKENS
|
|
303
|
+
) {
|
|
304
|
+
parsedBody.max_completion_tokens = KILO_FAILOVER_MAX_OUTPUT_TOKENS;
|
|
305
|
+
}
|
|
300
306
|
}
|
|
301
307
|
|
|
302
308
|
/**
|