llm-relay 0.65.3 → 0.67.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.
Files changed (40) hide show
  1. package/dist/accounting-store-schema.d.ts +18 -0
  2. package/dist/accounting-store-schema.js +9 -2
  3. package/dist/accounting-store-schema.js.map +1 -1
  4. package/dist/accounting-store.js +4 -0
  5. package/dist/accounting-store.js.map +1 -1
  6. package/dist/accounting.d.ts +22 -0
  7. package/dist/accounting.js +6 -0
  8. package/dist/accounting.js.map +1 -1
  9. package/dist/backend.d.ts +23 -0
  10. package/dist/backend.js +1 -0
  11. package/dist/backend.js.map +1 -1
  12. package/dist/circuit-breaker.d.ts +14 -0
  13. package/dist/circuit-breaker.js +20 -1
  14. package/dist/circuit-breaker.js.map +1 -1
  15. package/dist/cli.js +63 -48
  16. package/dist/cli.js.map +1 -1
  17. package/dist/config.d.ts +48 -0
  18. package/dist/config.js +33 -0
  19. package/dist/config.js.map +1 -1
  20. package/dist/credential-select.d.ts +34 -0
  21. package/dist/credential-select.js +48 -26
  22. package/dist/credential-select.js.map +1 -1
  23. package/dist/dashboard-contract.d.ts +31 -0
  24. package/dist/dashboard-contract.js +14 -5
  25. package/dist/dashboard-contract.js.map +1 -1
  26. package/dist/dashboard-snapshot.js +3 -0
  27. package/dist/dashboard-snapshot.js.map +1 -1
  28. package/dist/hedge-race.d.ts +90 -0
  29. package/dist/hedge-race.js +48 -0
  30. package/dist/hedge-race.js.map +1 -0
  31. package/dist/hedge-trigger.d.ts +143 -0
  32. package/dist/hedge-trigger.js +55 -0
  33. package/dist/hedge-trigger.js.map +1 -0
  34. package/dist/kernel/contracts.d.ts +18 -0
  35. package/dist/ping/metrics.d.ts +22 -4
  36. package/dist/ping/metrics.js +20 -19
  37. package/dist/ping/metrics.js.map +1 -1
  38. package/dist/server.js +361 -179
  39. package/dist/server.js.map +1 -1
  40. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -13,7 +13,7 @@ import { emitSse, emitSseTail, syntheticMessageId } from "./emitSse.js";
13
13
  import { repair, destructiveMatcher } from "./repair.js";
14
14
  import { CredentialWalkReshaper, HttpReshaper, } from "./reshaper.js";
15
15
  import { DIALECT_REFUSED_DESTRUCTIVE_CODE } from "./tool-dialects.js";
16
- import { dialectRefusalSignalOf, ERROR_ORIGIN_HEADER, TOOL_DIALECT_HEADER, fetchBackend, fetchOpenAiFront, normalizeOpenAiErrorBody, parseRetryAfterMs, postHeaderBodyFailure, upstreamReportedModel, toolUseIdRewrites, toolCallIdRewrites, thoughtSignatureSentinels, SERVED_BY_HEADER, POOL_ATTEMPTS_HEADER, UNKNOWN_REFUSAL_HEADER, DEGRADED_HEADER, PAID_HEADER, QUOTA_DEMOTED_HEADER, LATENCY_DEMOTED_HEADER, CREDENTIAL_HEADER, CREDENTIAL_ATTEMPTS_HEADER, HARD_CAP_HEADER, errorOrigin } from "./backend.js";
16
+ import { dialectRefusalSignalOf, ERROR_ORIGIN_HEADER, TOOL_DIALECT_HEADER, fetchBackend, fetchOpenAiFront, normalizeOpenAiErrorBody, parseRetryAfterMs, postHeaderBodyFailure, upstreamReportedModel, toolUseIdRewrites, toolCallIdRewrites, thoughtSignatureSentinels, SERVED_BY_HEADER, POOL_ATTEMPTS_HEADER, UNKNOWN_REFUSAL_HEADER, DEGRADED_HEADER, PAID_HEADER, QUOTA_DEMOTED_HEADER, LATENCY_DEMOTED_HEADER, HEDGED_HEADER, CREDENTIAL_HEADER, CREDENTIAL_ATTEMPTS_HEADER, HARD_CAP_HEADER, errorOrigin } from "./backend.js";
17
17
  import { probeStreamForCommit } from "./stream-commit.js";
18
18
  import { ModelCatalog } from "./catalog.js";
19
19
  import { handleAdminRoutes } from "./routes/admin.js";
@@ -42,6 +42,8 @@ import { looksLikeRateLimitError, parseStatedRateLimit, recordObservedRateLimit
42
42
  import { clearFacts, cooldownUntil, factsFor, isCostBlocked, recordFact } from "./target-facts.js";
43
43
  import { createQuotaDemotionFn, quotaDemotionLabel } from "./quota-demotion.js";
44
44
  import { createLatencyDemotionFn, latencyDemotionLabel } from "./latency-demotion.js";
45
+ import { hedgeDelayDecision, resolveHedgeSettings } from "./hedge-trigger.js";
46
+ import { raceWithHedge } from "./hedge-race.js";
45
47
  import { createHardCapLedgerReader, evaluateHardCap, hardCapLabel } from "./hard-cap.js";
46
48
  import { applyResetRule, interpretRefusal, materializeScope, parseStatedResetMs, recordUnknownRefusal } from "./refusal-interpretation.js";
47
49
  import { CONTROL_AUTHORIZATION_HEADER, createControlAuthorization, resolveControlAuthorizationConfigDir, validateControlAuthorization, } from "./control-authorization.js";
@@ -228,6 +230,13 @@ export function createProxy(cfg, deps = {}) {
228
230
  return undefined;
229
231
  }
230
232
  };
233
+ const hedgeSettings = resolveHedgeSettings(cfg.routing?.hedge);
234
+ const hedgeDelay = (attempt) => {
235
+ const t = attempt.target;
236
+ if (!t.model)
237
+ return null;
238
+ return hedgeDelayDecision(pingLoop.getModelPings(t.provider, t.model), costClassOf(attempt) === "free", hedgeSettings);
239
+ };
231
240
  const hardCapEvaluator = (attempt, now) => {
232
241
  try {
233
242
  const parsed = parseCredentialId(attempt.credentialId);
@@ -329,6 +338,8 @@ export function createProxy(cfg, deps = {}) {
329
338
  ...(controlAuthorization ? { controlAuthorization } : {}),
330
339
  quotaDemotion,
331
340
  latencyDemotion,
341
+ hedgeDelay,
342
+ hedgeMaxInFlight: hedgeSettings.enabled ? 2 : 1,
332
343
  costClassOf,
333
344
  hardCap: hardCapEvaluator,
334
345
  }).catch((e) => {
@@ -502,7 +513,7 @@ class RequestAccountingState {
502
513
  catch {
503
514
  }
504
515
  }
505
- complete(attempt, outcome, failureKind, usage, endedAt = Date.now()) {
516
+ complete(attempt, outcome, failureKind, usage, endedAt = Date.now(), abandonedByRelay = false) {
506
517
  if (attempt === null)
507
518
  return;
508
519
  try {
@@ -511,6 +522,7 @@ class RequestAccountingState {
511
522
  failureKind,
512
523
  endedAt,
513
524
  tokens: accountingTokens(usage, attempt.role === "serve" ? this.estimatedInputTokens : 0, attempt.role === "repair" || this.committed.has(attempt)),
525
+ abandonedByRelay,
514
526
  });
515
527
  }
516
528
  catch {
@@ -869,6 +881,7 @@ async function handle(req, res, cfg, h) {
869
881
  walkBudgetMs: cfg.walkBudgetMs ?? DEFAULT_WALK_BUDGET_MS,
870
882
  selectionNow: routingNow,
871
883
  evidenceFor: (attempt) => credentialEvidence(attempt, cfg, h.breaker, routingNow),
884
+ maxInFlight: h.hedgeMaxInFlight,
872
885
  });
873
886
  const credentialTrace = new CredentialAttemptTrace(cfg);
874
887
  if (openAiFrontProtocol) {
@@ -909,29 +922,41 @@ async function handle(req, res, cfg, h) {
909
922
  const pool429 = new Pool429Tracker();
910
923
  const tried = [];
911
924
  while (!res.destroyed) {
912
- const resolvedAttempt = nextUncappedAttempt(h, credentialWalk, attemptTrace, pool429);
913
- if (!resolvedAttempt)
925
+ const primaryOffer = nextUncappedAttempt(h, credentialWalk, attemptTrace, pool429);
926
+ if (!primaryOffer)
914
927
  break;
915
- target = resolvedAttempt.target;
916
- const controller = new AbortController();
917
- const callerController = new AbortController();
918
- const timer = setTimeout(() => controller.abort(), target.timeoutMs);
919
- const onResClose = abortOnClientClose(res, callerController, controller);
920
- res.on("close", onResClose);
928
+ const startAttempt = (run, forwardHeaders) => fetchBackend(run.resolvedAttempt, {
929
+ path,
930
+ method: req.method ?? "POST",
931
+ reqBuf,
932
+ reqJson,
933
+ anthropicHeaders: forwardHeaders,
934
+ wantsStream,
935
+ isDestructive: h.isDestructive,
936
+ usage: run.usage,
937
+ signal: run.controller.signal,
938
+ onEgress: () => {
939
+ run.egressCallbackCalled = true;
940
+ pool429.noteEgress();
941
+ const egressAt = Date.now();
942
+ run.attempt = beginHealthAttempt(h, run.resolvedAttempt, egressAt, attemptTrace, run.usage, accounting) ?? undefined;
943
+ if (!run.attempt)
944
+ throw new Error("llm-relay: could not begin provider attempt");
945
+ run.attempt.accountingAttempt = accounting?.startServe(run.resolvedAttempt, egressAt) ?? null;
946
+ recordCredentialStarted(credentialWalk, credentialTrace, run.resolvedAttempt);
947
+ tried.push(specOfTarget(run.target));
948
+ },
949
+ });
950
+ const primaryRun = beginAttemptRun(res, primaryOffer);
951
+ let resolvedAttempt = primaryRun.resolvedAttempt;
952
+ target = primaryRun.target;
953
+ let timer = primaryRun.timer;
954
+ let onResClose = primaryRun.onResClose;
955
+ let controller;
956
+ let callerController;
957
+ let egressCallbackCalled;
958
+ let hedged;
921
959
  let attempt;
922
- const usage = createUsageAccumulator();
923
- let egressCallbackCalled = false;
924
- const onEgress = () => {
925
- egressCallbackCalled = true;
926
- pool429.noteEgress();
927
- const egressAt = Date.now();
928
- attempt = beginHealthAttempt(h, resolvedAttempt, egressAt, attemptTrace, usage, accounting) ?? undefined;
929
- if (!attempt)
930
- throw new Error("llm-relay: could not begin provider attempt");
931
- attempt.accountingAttempt = accounting?.startServe(resolvedAttempt, egressAt) ?? null;
932
- recordCredentialStarted(credentialWalk, credentialTrace, resolvedAttempt);
933
- tried.push(specOfTarget(target));
934
- };
935
960
  let credentialRecorded = false;
936
961
  try {
937
962
  let forwardHeaders;
@@ -948,68 +973,87 @@ async function handle(req, res, cfg, h) {
948
973
  throw e;
949
974
  }
950
975
  let backendRes;
951
- try {
952
- backendRes = await fetchBackend(resolvedAttempt, {
953
- path,
954
- method: req.method ?? "POST",
955
- reqBuf,
956
- reqJson,
957
- anthropicHeaders: forwardHeaders,
958
- wantsStream,
959
- isDestructive: h.isDestructive,
960
- usage,
961
- signal: controller.signal,
962
- onEgress,
976
+ {
977
+ const raced = await runAttemptWithHedge({ run: primaryRun, promise: startAttempt(primaryRun, forwardHeaders) }, {
978
+ h,
979
+ res,
980
+ walk: credentialWalk,
981
+ credentialTrace,
982
+ attemptTrace,
983
+ tracker: pool429,
984
+ startRun: (offer) => {
985
+ const hedgeRun = beginAttemptRun(res, offer);
986
+ try {
987
+ return { run: hedgeRun, promise: startAttempt(hedgeRun, buildForwardHeaders(req.headers, offer)) };
988
+ }
989
+ catch {
990
+ releaseAttemptRun(res, hedgeRun);
991
+ credentialWalk.recordRejected(offer);
992
+ return undefined;
993
+ }
994
+ },
963
995
  });
964
- if (!attempt) {
965
- credentialWalk.recordRejected(resolvedAttempt);
966
- if (errorOrigin(backendRes) === "local") {
967
- await forwardLocalResponse(res, backendRes);
968
- h.logger.write(baseLog(started, path, hadTools, false, backendRes.status, "skipped", null, attemptTrace.snapshot()));
996
+ resolvedAttempt = raced.run.resolvedAttempt;
997
+ target = raced.run.target;
998
+ controller = raced.run.controller;
999
+ callerController = raced.run.callerController;
1000
+ timer = raced.run.timer;
1001
+ onResClose = raced.run.onResClose;
1002
+ attempt = raced.run.attempt;
1003
+ egressCallbackCalled = raced.run.egressCallbackCalled;
1004
+ hedged = raced.hedged;
1005
+ const settled = raced.settled;
1006
+ if (!settled.ok) {
1007
+ const e = settled.error;
1008
+ if (!attempt) {
1009
+ if (credentialWalk.isPending(resolvedAttempt)) {
1010
+ credentialWalk.recordRejected(resolvedAttempt);
1011
+ }
1012
+ if (res.destroyed)
1013
+ return;
1014
+ const status = controller.signal.aborted ? 504 : 502;
1015
+ failClosed(res, status, egressCallbackCalled
1016
+ ? "llm-relay: could not begin provider attempt"
1017
+ : "llm-relay: backend preparation failed");
1018
+ h.logger.write(baseLog(started, path, hadTools, false, status, "skipped", null, attemptTrace.snapshot()));
1019
+ return;
969
1020
  }
970
- else {
971
- await backendRes.body?.cancel().catch(() => { });
972
- failClosed(res, 502, "llm-relay: backend returned before provider egress");
973
- h.logger.write(baseLog(started, path, hadTools, false, 502, "skipped", null, attemptTrace.snapshot()));
1021
+ const aborted = controller.signal.aborted;
1022
+ const status = aborted ? 504 : 502;
1023
+ if (res.destroyed) {
1024
+ completeAttemptCancelled(h, attempt, "client disconnected");
1025
+ recordCredentialOutcome(credentialWalk, credentialTrace, resolvedAttempt, { kind: "cancelled" });
1026
+ credentialRecorded = true;
1027
+ return;
974
1028
  }
1029
+ completeAttemptFailure(h, attempt, {
1030
+ failure: "transport",
1031
+ provenance: aborted ? "deadline" : "upstream",
1032
+ status,
1033
+ });
1034
+ recordCredentialOutcome(credentialWalk, credentialTrace, resolvedAttempt, aborted ? { kind: "timeout" } : { kind: "provider-transport" });
1035
+ credentialRecorded = true;
1036
+ const walkEnd = endWalk(h, res, "anthropic", credentialWalk, attemptTrace, pool429, status, sticky, credentialTrace, () => baseLog(started, path, hadTools, false, status, "skipped", target, attemptTrace.snapshot()), {
1037
+ kind: "transport",
1038
+ message: aborted ? "backend timed out" : `backend unreachable: ${e.message}`,
1039
+ });
1040
+ if (walkEnd)
1041
+ continue;
975
1042
  return;
976
1043
  }
1044
+ backendRes = settled.value;
977
1045
  }
978
- catch (e) {
979
- if (!attempt) {
980
- if (credentialWalk.pending === resolvedAttempt) {
981
- credentialWalk.recordRejected(resolvedAttempt);
982
- }
983
- if (res.destroyed)
984
- return;
985
- const status = controller.signal.aborted ? 504 : 502;
986
- failClosed(res, status, egressCallbackCalled
987
- ? "llm-relay: could not begin provider attempt"
988
- : "llm-relay: backend preparation failed");
989
- h.logger.write(baseLog(started, path, hadTools, false, status, "skipped", null, attemptTrace.snapshot()));
990
- return;
1046
+ if (!attempt) {
1047
+ credentialWalk.recordRejected(resolvedAttempt);
1048
+ if (errorOrigin(backendRes) === "local") {
1049
+ await forwardLocalResponse(res, backendRes);
1050
+ h.logger.write(baseLog(started, path, hadTools, false, backendRes.status, "skipped", null, attemptTrace.snapshot()));
991
1051
  }
992
- const aborted = controller.signal.aborted;
993
- const status = aborted ? 504 : 502;
994
- if (res.destroyed) {
995
- completeAttemptCancelled(h, attempt, "client disconnected");
996
- recordCredentialOutcome(credentialWalk, credentialTrace, resolvedAttempt, { kind: "cancelled" });
997
- credentialRecorded = true;
998
- return;
1052
+ else {
1053
+ await backendRes.body?.cancel().catch(() => { });
1054
+ failClosed(res, 502, "llm-relay: backend returned before provider egress");
1055
+ h.logger.write(baseLog(started, path, hadTools, false, 502, "skipped", null, attemptTrace.snapshot()));
999
1056
  }
1000
- completeAttemptFailure(h, attempt, {
1001
- failure: "transport",
1002
- provenance: aborted ? "deadline" : "upstream",
1003
- status,
1004
- });
1005
- recordCredentialOutcome(credentialWalk, credentialTrace, resolvedAttempt, aborted ? { kind: "timeout" } : { kind: "provider-transport" });
1006
- credentialRecorded = true;
1007
- const walkEnd = endWalk(h, res, "anthropic", credentialWalk, attemptTrace, pool429, status, sticky, credentialTrace, () => baseLog(started, path, hadTools, false, status, "skipped", target, attemptTrace.snapshot()), {
1008
- kind: "transport",
1009
- message: aborted ? "backend timed out" : `backend unreachable: ${e.message}`,
1010
- });
1011
- if (walkEnd)
1012
- continue;
1013
1057
  return;
1014
1058
  }
1015
1059
  const reportedModelSource = backendRes;
@@ -1133,6 +1177,7 @@ async function handle(req, res, cfg, h) {
1133
1177
  degraded: degradedLabel(addressedPool, degradedSpecs, target),
1134
1178
  quotaDemoted: quotaDemotedFirst,
1135
1179
  latencyDemoted: latencyDemotedFirst,
1180
+ hedged,
1136
1181
  paid: paidLabel(cfg, h, target),
1137
1182
  credentialHeaders: backendRes.status < 400
1138
1183
  ? credentialTrace.headers(resolvedAttempt)
@@ -1201,7 +1246,7 @@ async function handle(req, res, cfg, h) {
1201
1246
  recordCredentialOutcome(credentialWalk, credentialTrace, resolvedAttempt, res.destroyed ? { kind: "cancelled" } : { kind: "local" });
1202
1247
  credentialRecorded = true;
1203
1248
  }
1204
- else if (!credentialRecorded && credentialWalk.pending === resolvedAttempt) {
1249
+ else if (!credentialRecorded && credentialWalk.isPending(resolvedAttempt)) {
1205
1250
  credentialWalk.recordRejected(resolvedAttempt);
1206
1251
  }
1207
1252
  if (attempt && !attempt.completed) {
@@ -1391,11 +1436,12 @@ class CredentialAttemptTrace {
1391
1436
  });
1392
1437
  }
1393
1438
  record(attempt, outcome) {
1394
- const pending = [...this.entries].reverse().find((entry) => entry.outcome === undefined);
1395
- if (!pending ||
1396
- pending.provider !== attempt.target.provider ||
1397
- pending.deployment !== specOfTarget(attempt.target) ||
1398
- pending.credentialId !== attempt.credentialId) {
1439
+ const deployment = specOfTarget(attempt.target);
1440
+ const pending = [...this.entries].reverse().find((entry) => entry.outcome === undefined &&
1441
+ entry.provider === attempt.target.provider &&
1442
+ entry.deployment === deployment &&
1443
+ entry.credentialId === attempt.credentialId);
1444
+ if (!pending) {
1399
1445
  throw new Error("credential attempt trace outcome does not match a started attempt");
1400
1446
  }
1401
1447
  pending.outcome = Object.freeze({ ...outcome });
@@ -1470,6 +1516,95 @@ function nextUncappedAttempt(h, walk, attemptTrace, tracker) {
1470
1516
  walk.recordRejected(candidate);
1471
1517
  }
1472
1518
  }
1519
+ function beginAttemptRun(res, offer) {
1520
+ const target = offer.target;
1521
+ const controller = new AbortController();
1522
+ const callerController = new AbortController();
1523
+ const run = {
1524
+ resolvedAttempt: offer,
1525
+ target,
1526
+ controller,
1527
+ callerController,
1528
+ timer: setTimeout(() => controller.abort(), target.timeoutMs),
1529
+ onResClose: abortOnClientClose(res, callerController, controller),
1530
+ usage: createUsageAccumulator(),
1531
+ attempt: undefined,
1532
+ egressCallbackCalled: false,
1533
+ };
1534
+ res.on("close", run.onResClose);
1535
+ return run;
1536
+ }
1537
+ function releaseAttemptRun(res, run) {
1538
+ clearTimeout(run.timer);
1539
+ res.off("close", run.onResClose);
1540
+ }
1541
+ function settleResponse(promise) {
1542
+ return promise.then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
1543
+ }
1544
+ function walkWouldFailOver(response) {
1545
+ return errorOrigin(response) !== "local" && shouldTryNext(classifyStatus(response.status));
1546
+ }
1547
+ function hedgedLabel(primary, hedge, winner, decision) {
1548
+ const side = winner === "hedge" ? "hedge won" : "primary won";
1549
+ return `${specOfTarget(primary.target)} -> ${specOfTarget(hedge.target)} (${side} after ${decision.delayMs}ms, ${decision.basis})`;
1550
+ }
1551
+ function retireHedgeLoser(deps, loser) {
1552
+ releaseAttemptRun(deps.res, loser);
1553
+ try {
1554
+ loser.controller.abort();
1555
+ }
1556
+ catch {
1557
+ }
1558
+ if (loser.attempt) {
1559
+ completeAttemptAbandoned(deps.h, loser.attempt, "hedge loser aborted");
1560
+ deps.credentialTrace.record(loser.resolvedAttempt, { kind: "cancelled" });
1561
+ deps.walk.recordAbandoned(loser.resolvedAttempt);
1562
+ }
1563
+ else if (deps.walk.isPending(loser.resolvedAttempt)) {
1564
+ deps.walk.recordRejected(loser.resolvedAttempt);
1565
+ }
1566
+ }
1567
+ async function runAttemptWithHedge(primary, deps) {
1568
+ const decision = deps.h.hedgeDelay(primary.run.resolvedAttempt);
1569
+ if (decision === null) {
1570
+ return { run: primary.run, settled: await settleResponse(primary.promise), hedged: null };
1571
+ }
1572
+ let hedge;
1573
+ let raced;
1574
+ try {
1575
+ raced = await raceWithHedge({
1576
+ primary: { promise: primary.promise, abort: () => primary.run.controller.abort() },
1577
+ delayMs: decision.delayMs,
1578
+ startHedge: () => {
1579
+ if (deps.res.destroyed)
1580
+ return undefined;
1581
+ const offer = nextUncappedAttempt(deps.h, deps.walk, deps.attemptTrace, deps.tracker);
1582
+ if (!offer)
1583
+ return undefined;
1584
+ if (offer === primary.run.resolvedAttempt)
1585
+ return undefined;
1586
+ const started = deps.startRun(offer);
1587
+ if (!started)
1588
+ return undefined;
1589
+ hedge = started;
1590
+ return { promise: started.promise, abort: () => started.run.controller.abort() };
1591
+ },
1592
+ isWin: (settled) => settled.ok && !walkWouldFailOver(settled.value),
1593
+ });
1594
+ }
1595
+ catch (e) {
1596
+ if (hedge)
1597
+ retireHedgeLoser(deps, hedge.run);
1598
+ throw e;
1599
+ }
1600
+ if (!raced.hedgeStarted || !hedge) {
1601
+ return { run: primary.run, settled: raced.settled, hedged: null };
1602
+ }
1603
+ const winner = raced.winner === "hedge" ? hedge.run : primary.run;
1604
+ const loser = raced.winner === "hedge" ? primary.run : hedge.run;
1605
+ retireHedgeLoser(deps, loser);
1606
+ return { run: winner, settled: raced.settled, hedged: hedgedLabel(primary.run, hedge.run, raced.winner, decision) };
1607
+ }
1473
1608
  function walkExitHeaders(tracker, sticky, credentialTrace, { errorOrigin, errorType, servedBy, } = {}) {
1474
1609
  const before = {};
1475
1610
  if (servedBy !== undefined)
@@ -1818,6 +1953,7 @@ function beginHealthAttempt(h, resolvedAttempt, started, trace, usage, accountin
1818
1953
  if (!begun.ok)
1819
1954
  return null;
1820
1955
  return {
1956
+ committed: false,
1821
1957
  handle: begun.value,
1822
1958
  identity,
1823
1959
  resolvedAttempt,
@@ -2091,6 +2227,7 @@ function accountingFailureForAttempt(options) {
2091
2227
  return "provider_error";
2092
2228
  }
2093
2229
  function markAttemptCommitted(attempt) {
2230
+ attempt.committed = true;
2094
2231
  attempt.accounting?.markCommitted(attempt.accountingAttempt, Date.now());
2095
2232
  }
2096
2233
  function completeAttemptSuccess(h, attempt, status) {
@@ -2144,6 +2281,12 @@ function completeAttemptFailure(h, attempt, options) {
2144
2281
  attempt.accounting?.complete(attempt.accountingAttempt, "error", accountingFailureForAttempt(options), attempt.usage, completedAt);
2145
2282
  }
2146
2283
  function completeAttemptCancelled(h, attempt, reason) {
2284
+ completeCancellation(h, attempt, reason, attempt.committed ? "client-gone-mid-response" : "client-gone-before-response");
2285
+ }
2286
+ function completeAttemptAbandoned(h, attempt, reason) {
2287
+ completeCancellation(h, attempt, reason, "relay-abandoned");
2288
+ }
2289
+ function completeCancellation(h, attempt, reason, cause) {
2147
2290
  if (attempt.completed)
2148
2291
  return;
2149
2292
  const completedAt = Date.now();
@@ -2153,6 +2296,7 @@ function completeAttemptCancelled(h, attempt, reason) {
2153
2296
  provenance: "client-cancellation",
2154
2297
  completedAt,
2155
2298
  elapsedMs: completedAt - attempt.started,
2299
+ cause,
2156
2300
  reason,
2157
2301
  });
2158
2302
  if (!result.ok)
@@ -2160,7 +2304,7 @@ function completeAttemptCancelled(h, attempt, reason) {
2160
2304
  attempt.completed = true;
2161
2305
  attempt.terminal = "cancelled";
2162
2306
  attempt.trace.record(attempt.target, "cancelled", attempt.started, completedAt);
2163
- attempt.accounting?.complete(attempt.accountingAttempt, "cancelled", "aborted", attempt.usage, completedAt);
2307
+ attempt.accounting?.complete(attempt.accountingAttempt, "cancelled", "aborted", attempt.usage, completedAt, cause === "relay-abandoned");
2164
2308
  }
2165
2309
  function completePostHeaderBodyFailure(h, downstream, signal, attempt, credentialWalk, credentialTrace, resolvedAttempt) {
2166
2310
  if (downstream.destroyed) {
@@ -2200,45 +2344,13 @@ async function openAiFrontPath(res, credentialWalk, credentialTrace, ctx, h) {
2200
2344
  const directTools = toolSchemaMap(ctx.reqJson);
2201
2345
  const pool429 = new Pool429Tracker();
2202
2346
  while (!res.destroyed) {
2203
- const resolvedAttempt = nextUncappedAttempt(h, credentialWalk, attemptTrace, pool429);
2204
- if (!resolvedAttempt)
2347
+ const primaryOffer = nextUncappedAttempt(h, credentialWalk, attemptTrace, pool429);
2348
+ if (!primaryOffer)
2205
2349
  break;
2206
- const target = resolvedAttempt.target;
2207
- const controller = new AbortController();
2208
- const callerController = new AbortController();
2209
- const timer = setTimeout(() => controller.abort(), target.timeoutMs);
2210
- const onResClose = abortOnClientClose(res, callerController, controller);
2211
- res.on("close", onResClose);
2212
- let attempt;
2213
- const usage = createUsageAccumulator();
2214
- let egressCallbackCalled = false;
2215
- const onEgress = () => {
2216
- egressCallbackCalled = true;
2217
- pool429.noteEgress();
2218
- const egressAt = Date.now();
2219
- attempt = beginHealthAttempt(h, resolvedAttempt, egressAt, attemptTrace, usage, ctx.accounting) ?? undefined;
2220
- if (!attempt)
2221
- throw new Error("llm-relay: could not begin provider attempt");
2222
- attempt.accountingAttempt = ctx.accounting?.startServe(resolvedAttempt, egressAt) ?? null;
2223
- recordCredentialStarted(credentialWalk, credentialTrace, resolvedAttempt);
2224
- tried.push(specOfTarget(target));
2225
- };
2226
- let credentialRecorded = false;
2227
- try {
2228
- let forwardHeaders;
2229
- try {
2230
- forwardHeaders = buildForwardHeaders(ctx.inboundHeaders, resolvedAttempt);
2231
- }
2232
- catch (e) {
2233
- credentialWalk.recordRejected(resolvedAttempt);
2234
- if (e instanceof CredentialConfigError) {
2235
- failClosed(res, 502, `llm-relay configuration: ${e.message}`);
2236
- h.logger.write(baseLog(ctx.started, ctx.path, ctx.hadTools, false, 502, "skipped", null, attemptTrace.snapshot()));
2237
- return;
2238
- }
2239
- throw e;
2240
- }
2350
+ const audits = new Map();
2351
+ const startAttempt = (run, forwardHeaders) => {
2241
2352
  const recoveryAudit = { value: null };
2353
+ audits.set(run, recoveryAudit);
2242
2354
  const processRecoveredChat = async (recovered) => {
2243
2355
  const assistant = {
2244
2356
  content: [
@@ -2264,7 +2376,7 @@ async function openAiFrontPath(res, credentialWalk, credentialTrace, ctx, h) {
2264
2376
  };
2265
2377
  if (validation.valid || ctx.cfg?.mode !== "repair")
2266
2378
  return recovered;
2267
- const rawReshaper = h.resolveReshaper(resolvedAttempt);
2379
+ const rawReshaper = h.resolveReshaper(run.resolvedAttempt);
2268
2380
  const reshaper = rawReshaper ? withRepairAccounting(rawReshaper, ctx.accounting) : undefined;
2269
2381
  if (!reshaper)
2270
2382
  return recovered;
@@ -2273,8 +2385,8 @@ async function openAiFrontPath(res, credentialWalk, credentialTrace, ctx, h) {
2273
2385
  reshaper,
2274
2386
  maxAttempts: ctx.cfg.repair.maxAttempts,
2275
2387
  isDestructive: h.isDestructive,
2276
- backendModel: target.model ?? null,
2277
- signal: callerController.signal,
2388
+ backendModel: run.target.model ?? null,
2389
+ signal: run.callerController.signal,
2278
2390
  });
2279
2391
  recoveryAudit.value.repair = decision.outcome;
2280
2392
  if (decision.outcome !== "fixed" || !decision.message) {
@@ -2299,69 +2411,136 @@ async function openAiFrontPath(res, credentialWalk, credentialTrace, ctx, h) {
2299
2411
  }
2300
2412
  return fixed;
2301
2413
  };
2302
- let upstream;
2414
+ return fetchOpenAiFront(run.resolvedAttempt, {
2415
+ reqJson: ctx.reqJson,
2416
+ wantsStream: ctx.wantsStream,
2417
+ protocol: ctx.protocol,
2418
+ anthropicHeaders: forwardHeaders,
2419
+ signal: run.controller.signal,
2420
+ isDestructive: h.isDestructive,
2421
+ processRecoveredChat,
2422
+ usage: run.usage,
2423
+ onEgress: () => {
2424
+ run.egressCallbackCalled = true;
2425
+ pool429.noteEgress();
2426
+ const egressAt = Date.now();
2427
+ run.attempt = beginHealthAttempt(h, run.resolvedAttempt, egressAt, attemptTrace, run.usage, ctx.accounting) ?? undefined;
2428
+ if (!run.attempt)
2429
+ throw new Error("llm-relay: could not begin provider attempt");
2430
+ run.attempt.accountingAttempt = ctx.accounting?.startServe(run.resolvedAttempt, egressAt) ?? null;
2431
+ recordCredentialStarted(credentialWalk, credentialTrace, run.resolvedAttempt);
2432
+ tried.push(specOfTarget(run.target));
2433
+ },
2434
+ });
2435
+ };
2436
+ const primaryRun = beginAttemptRun(res, primaryOffer);
2437
+ let resolvedAttempt = primaryRun.resolvedAttempt;
2438
+ let target = primaryRun.target;
2439
+ let timer = primaryRun.timer;
2440
+ let onResClose = primaryRun.onResClose;
2441
+ let controller;
2442
+ let egressCallbackCalled;
2443
+ let hedged;
2444
+ let recoveryAudit;
2445
+ let attempt;
2446
+ let credentialRecorded = false;
2447
+ try {
2448
+ let forwardHeaders;
2303
2449
  try {
2304
- upstream = await fetchOpenAiFront(resolvedAttempt, {
2305
- reqJson: ctx.reqJson,
2306
- wantsStream: ctx.wantsStream,
2307
- protocol: ctx.protocol,
2308
- anthropicHeaders: forwardHeaders,
2309
- signal: controller.signal,
2310
- isDestructive: h.isDestructive,
2311
- processRecoveredChat,
2312
- usage,
2313
- onEgress,
2314
- });
2315
- if (!attempt) {
2316
- credentialWalk.recordRejected(resolvedAttempt);
2317
- if (errorOrigin(upstream) === "local") {
2318
- await forwardLocalResponse(res, upstream);
2319
- h.logger.write(baseLog(ctx.started, ctx.path, ctx.hadTools, false, upstream.status, "skipped", null, attemptTrace.snapshot()));
2320
- }
2321
- else {
2322
- await upstream.body?.cancel().catch(() => { });
2323
- failClosed(res, 502, "llm-relay: backend returned before provider egress");
2324
- h.logger.write(baseLog(ctx.started, ctx.path, ctx.hadTools, false, 502, "skipped", null, attemptTrace.snapshot()));
2325
- }
2450
+ forwardHeaders = buildForwardHeaders(ctx.inboundHeaders, resolvedAttempt);
2451
+ }
2452
+ catch (e) {
2453
+ credentialWalk.recordRejected(resolvedAttempt);
2454
+ if (e instanceof CredentialConfigError) {
2455
+ failClosed(res, 502, `llm-relay configuration: ${e.message}`);
2456
+ h.logger.write(baseLog(ctx.started, ctx.path, ctx.hadTools, false, 502, "skipped", null, attemptTrace.snapshot()));
2326
2457
  return;
2327
2458
  }
2459
+ throw e;
2328
2460
  }
2329
- catch (e) {
2330
- if (!attempt) {
2331
- if (credentialWalk.pending === resolvedAttempt) {
2332
- credentialWalk.recordRejected(resolvedAttempt);
2461
+ let upstream;
2462
+ {
2463
+ const raced = await runAttemptWithHedge({ run: primaryRun, promise: startAttempt(primaryRun, forwardHeaders) }, {
2464
+ h,
2465
+ res,
2466
+ walk: credentialWalk,
2467
+ credentialTrace,
2468
+ attemptTrace,
2469
+ tracker: pool429,
2470
+ startRun: (offer) => {
2471
+ const hedgeRun = beginAttemptRun(res, offer);
2472
+ try {
2473
+ return { run: hedgeRun, promise: startAttempt(hedgeRun, buildForwardHeaders(ctx.inboundHeaders, offer)) };
2474
+ }
2475
+ catch {
2476
+ releaseAttemptRun(res, hedgeRun);
2477
+ credentialWalk.recordRejected(offer);
2478
+ return undefined;
2479
+ }
2480
+ },
2481
+ });
2482
+ resolvedAttempt = raced.run.resolvedAttempt;
2483
+ target = raced.run.target;
2484
+ controller = raced.run.controller;
2485
+ timer = raced.run.timer;
2486
+ onResClose = raced.run.onResClose;
2487
+ attempt = raced.run.attempt;
2488
+ egressCallbackCalled = raced.run.egressCallbackCalled;
2489
+ hedged = raced.hedged;
2490
+ recoveryAudit = audits.get(raced.run) ?? { value: null };
2491
+ const settled = raced.settled;
2492
+ if (!settled.ok) {
2493
+ const e = settled.error;
2494
+ if (!attempt) {
2495
+ if (credentialWalk.isPending(resolvedAttempt)) {
2496
+ credentialWalk.recordRejected(resolvedAttempt);
2497
+ }
2498
+ if (res.destroyed)
2499
+ return;
2500
+ const status = controller.signal.aborted ? 504 : 502;
2501
+ failClosed(res, status, egressCallbackCalled
2502
+ ? "llm-relay: could not begin provider attempt"
2503
+ : "llm-relay: backend preparation failed");
2504
+ h.logger.write(baseLog(ctx.started, ctx.path, ctx.hadTools, false, status, "skipped", null, attemptTrace.snapshot()));
2505
+ return;
2333
2506
  }
2334
- if (res.destroyed)
2507
+ const aborted = controller.signal.aborted;
2508
+ const status = aborted ? 504 : 502;
2509
+ if (res.destroyed) {
2510
+ completeAttemptCancelled(h, attempt, "client disconnected");
2511
+ recordCredentialOutcome(credentialWalk, credentialTrace, resolvedAttempt, { kind: "cancelled" });
2512
+ credentialRecorded = true;
2335
2513
  return;
2336
- const status = controller.signal.aborted ? 504 : 502;
2337
- failClosed(res, status, egressCallbackCalled
2338
- ? "llm-relay: could not begin provider attempt"
2339
- : "llm-relay: backend preparation failed");
2340
- h.logger.write(baseLog(ctx.started, ctx.path, ctx.hadTools, false, status, "skipped", null, attemptTrace.snapshot()));
2341
- return;
2342
- }
2343
- const aborted = controller.signal.aborted;
2344
- const status = aborted ? 504 : 502;
2345
- if (res.destroyed) {
2346
- completeAttemptCancelled(h, attempt, "client disconnected");
2347
- recordCredentialOutcome(credentialWalk, credentialTrace, resolvedAttempt, { kind: "cancelled" });
2514
+ }
2515
+ completeAttemptFailure(h, attempt, {
2516
+ failure: "transport",
2517
+ provenance: aborted ? "deadline" : "upstream",
2518
+ status,
2519
+ });
2520
+ recordCredentialOutcome(credentialWalk, credentialTrace, resolvedAttempt, aborted ? { kind: "timeout" } : { kind: "provider-transport" });
2348
2521
  credentialRecorded = true;
2522
+ const walkEnd = endWalk(h, res, "openai", credentialWalk, attemptTrace, pool429, status, ctx.sticky, credentialTrace, () => baseLog(ctx.started, ctx.path, ctx.hadTools, false, status, "skipped", target, attemptTrace.snapshot()), {
2523
+ kind: "transport",
2524
+ message: aborted ? "backend timed out" : `backend unreachable: ${e.message}`,
2525
+ servedBy: tried.join(", "),
2526
+ });
2527
+ if (walkEnd)
2528
+ continue;
2349
2529
  return;
2350
2530
  }
2351
- completeAttemptFailure(h, attempt, {
2352
- failure: "transport",
2353
- provenance: aborted ? "deadline" : "upstream",
2354
- status,
2355
- });
2356
- recordCredentialOutcome(credentialWalk, credentialTrace, resolvedAttempt, aborted ? { kind: "timeout" } : { kind: "provider-transport" });
2357
- credentialRecorded = true;
2358
- const walkEnd = endWalk(h, res, "openai", credentialWalk, attemptTrace, pool429, status, ctx.sticky, credentialTrace, () => baseLog(ctx.started, ctx.path, ctx.hadTools, false, status, "skipped", target, attemptTrace.snapshot()), {
2359
- kind: "transport",
2360
- message: aborted ? "backend timed out" : `backend unreachable: ${e.message}`,
2361
- servedBy: tried.join(", "),
2362
- });
2363
- if (walkEnd)
2364
- continue;
2531
+ upstream = settled.value;
2532
+ }
2533
+ if (!attempt) {
2534
+ credentialWalk.recordRejected(resolvedAttempt);
2535
+ if (errorOrigin(upstream) === "local") {
2536
+ await forwardLocalResponse(res, upstream);
2537
+ h.logger.write(baseLog(ctx.started, ctx.path, ctx.hadTools, false, upstream.status, "skipped", null, attemptTrace.snapshot()));
2538
+ }
2539
+ else {
2540
+ await upstream.body?.cancel().catch(() => { });
2541
+ failClosed(res, 502, "llm-relay: backend returned before provider egress");
2542
+ h.logger.write(baseLog(ctx.started, ctx.path, ctx.hadTools, false, 502, "skipped", null, attemptTrace.snapshot()));
2543
+ }
2365
2544
  return;
2366
2545
  }
2367
2546
  const reportedModelSource = upstream;
@@ -2479,6 +2658,7 @@ async function openAiFrontPath(res, credentialWalk, credentialTrace, ctx, h) {
2479
2658
  degraded: degradedLabel(ctx.addressedPool ?? null, ctx.degradedSpecs ?? null, target),
2480
2659
  quotaDemoted: ctx.quotaDemotedFirst,
2481
2660
  latencyDemoted: ctx.latencyDemotedFirst,
2661
+ hedged,
2482
2662
  paid: ctx.cfg ? paidLabel(ctx.cfg, h, target) : null,
2483
2663
  sticky: ctx.sticky,
2484
2664
  });
@@ -2542,7 +2722,7 @@ async function openAiFrontPath(res, credentialWalk, credentialTrace, ctx, h) {
2542
2722
  recordCredentialOutcome(credentialWalk, credentialTrace, resolvedAttempt, res.destroyed ? { kind: "cancelled" } : { kind: "local" });
2543
2723
  credentialRecorded = true;
2544
2724
  }
2545
- else if (!credentialRecorded && credentialWalk.pending === resolvedAttempt) {
2725
+ else if (!credentialRecorded && credentialWalk.isPending(resolvedAttempt)) {
2546
2726
  credentialWalk.recordRejected(resolvedAttempt);
2547
2727
  }
2548
2728
  if (attempt && !attempt.completed) {
@@ -2592,6 +2772,8 @@ function responseHeadersForTarget(backendRes, ctx) {
2592
2772
  responseHeaders[QUOTA_DEMOTED_HEADER] = ctx.quotaDemoted;
2593
2773
  if (ctx.latencyDemoted)
2594
2774
  responseHeaders[LATENCY_DEMOTED_HEADER] = ctx.latencyDemoted;
2775
+ if (ctx.hedged)
2776
+ responseHeaders[HEDGED_HEADER] = ctx.hedged;
2595
2777
  if (ctx.paid)
2596
2778
  responseHeaders[PAID_HEADER] = ctx.paid;
2597
2779
  if (ctx.credentialHeaders)