llm-relay 0.65.2 → 0.66.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/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) => {
@@ -869,6 +880,7 @@ async function handle(req, res, cfg, h) {
869
880
  walkBudgetMs: cfg.walkBudgetMs ?? DEFAULT_WALK_BUDGET_MS,
870
881
  selectionNow: routingNow,
871
882
  evidenceFor: (attempt) => credentialEvidence(attempt, cfg, h.breaker, routingNow),
883
+ maxInFlight: h.hedgeMaxInFlight,
872
884
  });
873
885
  const credentialTrace = new CredentialAttemptTrace(cfg);
874
886
  if (openAiFrontProtocol) {
@@ -909,29 +921,41 @@ async function handle(req, res, cfg, h) {
909
921
  const pool429 = new Pool429Tracker();
910
922
  const tried = [];
911
923
  while (!res.destroyed) {
912
- const resolvedAttempt = nextUncappedAttempt(h, credentialWalk, attemptTrace, pool429);
913
- if (!resolvedAttempt)
924
+ const primaryOffer = nextUncappedAttempt(h, credentialWalk, attemptTrace, pool429);
925
+ if (!primaryOffer)
914
926
  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);
927
+ const startAttempt = (run, forwardHeaders) => fetchBackend(run.resolvedAttempt, {
928
+ path,
929
+ method: req.method ?? "POST",
930
+ reqBuf,
931
+ reqJson,
932
+ anthropicHeaders: forwardHeaders,
933
+ wantsStream,
934
+ isDestructive: h.isDestructive,
935
+ usage: run.usage,
936
+ signal: run.controller.signal,
937
+ onEgress: () => {
938
+ run.egressCallbackCalled = true;
939
+ pool429.noteEgress();
940
+ const egressAt = Date.now();
941
+ run.attempt = beginHealthAttempt(h, run.resolvedAttempt, egressAt, attemptTrace, run.usage, accounting) ?? undefined;
942
+ if (!run.attempt)
943
+ throw new Error("llm-relay: could not begin provider attempt");
944
+ run.attempt.accountingAttempt = accounting?.startServe(run.resolvedAttempt, egressAt) ?? null;
945
+ recordCredentialStarted(credentialWalk, credentialTrace, run.resolvedAttempt);
946
+ tried.push(specOfTarget(run.target));
947
+ },
948
+ });
949
+ const primaryRun = beginAttemptRun(res, primaryOffer);
950
+ let resolvedAttempt = primaryRun.resolvedAttempt;
951
+ target = primaryRun.target;
952
+ let timer = primaryRun.timer;
953
+ let onResClose = primaryRun.onResClose;
954
+ let controller;
955
+ let callerController;
956
+ let egressCallbackCalled;
957
+ let hedged;
921
958
  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
959
  let credentialRecorded = false;
936
960
  try {
937
961
  let forwardHeaders;
@@ -948,68 +972,87 @@ async function handle(req, res, cfg, h) {
948
972
  throw e;
949
973
  }
950
974
  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,
975
+ {
976
+ const raced = await runAttemptWithHedge({ run: primaryRun, promise: startAttempt(primaryRun, forwardHeaders) }, {
977
+ h,
978
+ res,
979
+ walk: credentialWalk,
980
+ credentialTrace,
981
+ attemptTrace,
982
+ tracker: pool429,
983
+ startRun: (offer) => {
984
+ const hedgeRun = beginAttemptRun(res, offer);
985
+ try {
986
+ return { run: hedgeRun, promise: startAttempt(hedgeRun, buildForwardHeaders(req.headers, offer)) };
987
+ }
988
+ catch {
989
+ releaseAttemptRun(res, hedgeRun);
990
+ credentialWalk.recordRejected(offer);
991
+ return undefined;
992
+ }
993
+ },
963
994
  });
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()));
995
+ resolvedAttempt = raced.run.resolvedAttempt;
996
+ target = raced.run.target;
997
+ controller = raced.run.controller;
998
+ callerController = raced.run.callerController;
999
+ timer = raced.run.timer;
1000
+ onResClose = raced.run.onResClose;
1001
+ attempt = raced.run.attempt;
1002
+ egressCallbackCalled = raced.run.egressCallbackCalled;
1003
+ hedged = raced.hedged;
1004
+ const settled = raced.settled;
1005
+ if (!settled.ok) {
1006
+ const e = settled.error;
1007
+ if (!attempt) {
1008
+ if (credentialWalk.isPending(resolvedAttempt)) {
1009
+ credentialWalk.recordRejected(resolvedAttempt);
1010
+ }
1011
+ if (res.destroyed)
1012
+ return;
1013
+ const status = controller.signal.aborted ? 504 : 502;
1014
+ failClosed(res, status, egressCallbackCalled
1015
+ ? "llm-relay: could not begin provider attempt"
1016
+ : "llm-relay: backend preparation failed");
1017
+ h.logger.write(baseLog(started, path, hadTools, false, status, "skipped", null, attemptTrace.snapshot()));
1018
+ return;
969
1019
  }
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()));
1020
+ const aborted = controller.signal.aborted;
1021
+ const status = aborted ? 504 : 502;
1022
+ if (res.destroyed) {
1023
+ completeAttemptCancelled(h, attempt, "client disconnected");
1024
+ recordCredentialOutcome(credentialWalk, credentialTrace, resolvedAttempt, { kind: "cancelled" });
1025
+ credentialRecorded = true;
1026
+ return;
974
1027
  }
1028
+ completeAttemptFailure(h, attempt, {
1029
+ failure: "transport",
1030
+ provenance: aborted ? "deadline" : "upstream",
1031
+ status,
1032
+ });
1033
+ recordCredentialOutcome(credentialWalk, credentialTrace, resolvedAttempt, aborted ? { kind: "timeout" } : { kind: "provider-transport" });
1034
+ credentialRecorded = true;
1035
+ const walkEnd = endWalk(h, res, "anthropic", credentialWalk, attemptTrace, pool429, status, sticky, credentialTrace, () => baseLog(started, path, hadTools, false, status, "skipped", target, attemptTrace.snapshot()), {
1036
+ kind: "transport",
1037
+ message: aborted ? "backend timed out" : `backend unreachable: ${e.message}`,
1038
+ });
1039
+ if (walkEnd)
1040
+ continue;
975
1041
  return;
976
1042
  }
1043
+ backendRes = settled.value;
977
1044
  }
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;
1045
+ if (!attempt) {
1046
+ credentialWalk.recordRejected(resolvedAttempt);
1047
+ if (errorOrigin(backendRes) === "local") {
1048
+ await forwardLocalResponse(res, backendRes);
1049
+ h.logger.write(baseLog(started, path, hadTools, false, backendRes.status, "skipped", null, attemptTrace.snapshot()));
991
1050
  }
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;
1051
+ else {
1052
+ await backendRes.body?.cancel().catch(() => { });
1053
+ failClosed(res, 502, "llm-relay: backend returned before provider egress");
1054
+ h.logger.write(baseLog(started, path, hadTools, false, 502, "skipped", null, attemptTrace.snapshot()));
999
1055
  }
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
1056
  return;
1014
1057
  }
1015
1058
  const reportedModelSource = backendRes;
@@ -1133,6 +1176,7 @@ async function handle(req, res, cfg, h) {
1133
1176
  degraded: degradedLabel(addressedPool, degradedSpecs, target),
1134
1177
  quotaDemoted: quotaDemotedFirst,
1135
1178
  latencyDemoted: latencyDemotedFirst,
1179
+ hedged,
1136
1180
  paid: paidLabel(cfg, h, target),
1137
1181
  credentialHeaders: backendRes.status < 400
1138
1182
  ? credentialTrace.headers(resolvedAttempt)
@@ -1201,7 +1245,7 @@ async function handle(req, res, cfg, h) {
1201
1245
  recordCredentialOutcome(credentialWalk, credentialTrace, resolvedAttempt, res.destroyed ? { kind: "cancelled" } : { kind: "local" });
1202
1246
  credentialRecorded = true;
1203
1247
  }
1204
- else if (!credentialRecorded && credentialWalk.pending === resolvedAttempt) {
1248
+ else if (!credentialRecorded && credentialWalk.isPending(resolvedAttempt)) {
1205
1249
  credentialWalk.recordRejected(resolvedAttempt);
1206
1250
  }
1207
1251
  if (attempt && !attempt.completed) {
@@ -1391,11 +1435,12 @@ class CredentialAttemptTrace {
1391
1435
  });
1392
1436
  }
1393
1437
  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) {
1438
+ const deployment = specOfTarget(attempt.target);
1439
+ const pending = [...this.entries].reverse().find((entry) => entry.outcome === undefined &&
1440
+ entry.provider === attempt.target.provider &&
1441
+ entry.deployment === deployment &&
1442
+ entry.credentialId === attempt.credentialId);
1443
+ if (!pending) {
1399
1444
  throw new Error("credential attempt trace outcome does not match a started attempt");
1400
1445
  }
1401
1446
  pending.outcome = Object.freeze({ ...outcome });
@@ -1470,6 +1515,95 @@ function nextUncappedAttempt(h, walk, attemptTrace, tracker) {
1470
1515
  walk.recordRejected(candidate);
1471
1516
  }
1472
1517
  }
1518
+ function beginAttemptRun(res, offer) {
1519
+ const target = offer.target;
1520
+ const controller = new AbortController();
1521
+ const callerController = new AbortController();
1522
+ const run = {
1523
+ resolvedAttempt: offer,
1524
+ target,
1525
+ controller,
1526
+ callerController,
1527
+ timer: setTimeout(() => controller.abort(), target.timeoutMs),
1528
+ onResClose: abortOnClientClose(res, callerController, controller),
1529
+ usage: createUsageAccumulator(),
1530
+ attempt: undefined,
1531
+ egressCallbackCalled: false,
1532
+ };
1533
+ res.on("close", run.onResClose);
1534
+ return run;
1535
+ }
1536
+ function releaseAttemptRun(res, run) {
1537
+ clearTimeout(run.timer);
1538
+ res.off("close", run.onResClose);
1539
+ }
1540
+ function settleResponse(promise) {
1541
+ return promise.then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
1542
+ }
1543
+ function walkWouldFailOver(response) {
1544
+ return errorOrigin(response) !== "local" && shouldTryNext(classifyStatus(response.status));
1545
+ }
1546
+ function hedgedLabel(primary, hedge, winner, decision) {
1547
+ const side = winner === "hedge" ? "hedge won" : "primary won";
1548
+ return `${specOfTarget(primary.target)} -> ${specOfTarget(hedge.target)} (${side} after ${decision.delayMs}ms, ${decision.basis})`;
1549
+ }
1550
+ function retireHedgeLoser(deps, loser) {
1551
+ releaseAttemptRun(deps.res, loser);
1552
+ try {
1553
+ loser.controller.abort();
1554
+ }
1555
+ catch {
1556
+ }
1557
+ if (loser.attempt) {
1558
+ completeAttemptCancelled(deps.h, loser.attempt, "hedge loser aborted");
1559
+ deps.credentialTrace.record(loser.resolvedAttempt, { kind: "cancelled" });
1560
+ deps.walk.recordAbandoned(loser.resolvedAttempt);
1561
+ }
1562
+ else if (deps.walk.isPending(loser.resolvedAttempt)) {
1563
+ deps.walk.recordRejected(loser.resolvedAttempt);
1564
+ }
1565
+ }
1566
+ async function runAttemptWithHedge(primary, deps) {
1567
+ const decision = deps.h.hedgeDelay(primary.run.resolvedAttempt);
1568
+ if (decision === null) {
1569
+ return { run: primary.run, settled: await settleResponse(primary.promise), hedged: null };
1570
+ }
1571
+ let hedge;
1572
+ let raced;
1573
+ try {
1574
+ raced = await raceWithHedge({
1575
+ primary: { promise: primary.promise, abort: () => primary.run.controller.abort() },
1576
+ delayMs: decision.delayMs,
1577
+ startHedge: () => {
1578
+ if (deps.res.destroyed)
1579
+ return undefined;
1580
+ const offer = nextUncappedAttempt(deps.h, deps.walk, deps.attemptTrace, deps.tracker);
1581
+ if (!offer)
1582
+ return undefined;
1583
+ if (offer === primary.run.resolvedAttempt)
1584
+ return undefined;
1585
+ const started = deps.startRun(offer);
1586
+ if (!started)
1587
+ return undefined;
1588
+ hedge = started;
1589
+ return { promise: started.promise, abort: () => started.run.controller.abort() };
1590
+ },
1591
+ isWin: (settled) => settled.ok && !walkWouldFailOver(settled.value),
1592
+ });
1593
+ }
1594
+ catch (e) {
1595
+ if (hedge)
1596
+ retireHedgeLoser(deps, hedge.run);
1597
+ throw e;
1598
+ }
1599
+ if (!raced.hedgeStarted || !hedge) {
1600
+ return { run: primary.run, settled: raced.settled, hedged: null };
1601
+ }
1602
+ const winner = raced.winner === "hedge" ? hedge.run : primary.run;
1603
+ const loser = raced.winner === "hedge" ? primary.run : hedge.run;
1604
+ retireHedgeLoser(deps, loser);
1605
+ return { run: winner, settled: raced.settled, hedged: hedgedLabel(primary.run, hedge.run, raced.winner, decision) };
1606
+ }
1473
1607
  function walkExitHeaders(tracker, sticky, credentialTrace, { errorOrigin, errorType, servedBy, } = {}) {
1474
1608
  const before = {};
1475
1609
  if (servedBy !== undefined)
@@ -2200,45 +2334,13 @@ async function openAiFrontPath(res, credentialWalk, credentialTrace, ctx, h) {
2200
2334
  const directTools = toolSchemaMap(ctx.reqJson);
2201
2335
  const pool429 = new Pool429Tracker();
2202
2336
  while (!res.destroyed) {
2203
- const resolvedAttempt = nextUncappedAttempt(h, credentialWalk, attemptTrace, pool429);
2204
- if (!resolvedAttempt)
2337
+ const primaryOffer = nextUncappedAttempt(h, credentialWalk, attemptTrace, pool429);
2338
+ if (!primaryOffer)
2205
2339
  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
- }
2340
+ const audits = new Map();
2341
+ const startAttempt = (run, forwardHeaders) => {
2241
2342
  const recoveryAudit = { value: null };
2343
+ audits.set(run, recoveryAudit);
2242
2344
  const processRecoveredChat = async (recovered) => {
2243
2345
  const assistant = {
2244
2346
  content: [
@@ -2264,7 +2366,7 @@ async function openAiFrontPath(res, credentialWalk, credentialTrace, ctx, h) {
2264
2366
  };
2265
2367
  if (validation.valid || ctx.cfg?.mode !== "repair")
2266
2368
  return recovered;
2267
- const rawReshaper = h.resolveReshaper(resolvedAttempt);
2369
+ const rawReshaper = h.resolveReshaper(run.resolvedAttempt);
2268
2370
  const reshaper = rawReshaper ? withRepairAccounting(rawReshaper, ctx.accounting) : undefined;
2269
2371
  if (!reshaper)
2270
2372
  return recovered;
@@ -2273,8 +2375,8 @@ async function openAiFrontPath(res, credentialWalk, credentialTrace, ctx, h) {
2273
2375
  reshaper,
2274
2376
  maxAttempts: ctx.cfg.repair.maxAttempts,
2275
2377
  isDestructive: h.isDestructive,
2276
- backendModel: target.model ?? null,
2277
- signal: callerController.signal,
2378
+ backendModel: run.target.model ?? null,
2379
+ signal: run.callerController.signal,
2278
2380
  });
2279
2381
  recoveryAudit.value.repair = decision.outcome;
2280
2382
  if (decision.outcome !== "fixed" || !decision.message) {
@@ -2299,69 +2401,136 @@ async function openAiFrontPath(res, credentialWalk, credentialTrace, ctx, h) {
2299
2401
  }
2300
2402
  return fixed;
2301
2403
  };
2302
- let upstream;
2404
+ return fetchOpenAiFront(run.resolvedAttempt, {
2405
+ reqJson: ctx.reqJson,
2406
+ wantsStream: ctx.wantsStream,
2407
+ protocol: ctx.protocol,
2408
+ anthropicHeaders: forwardHeaders,
2409
+ signal: run.controller.signal,
2410
+ isDestructive: h.isDestructive,
2411
+ processRecoveredChat,
2412
+ usage: run.usage,
2413
+ onEgress: () => {
2414
+ run.egressCallbackCalled = true;
2415
+ pool429.noteEgress();
2416
+ const egressAt = Date.now();
2417
+ run.attempt = beginHealthAttempt(h, run.resolvedAttempt, egressAt, attemptTrace, run.usage, ctx.accounting) ?? undefined;
2418
+ if (!run.attempt)
2419
+ throw new Error("llm-relay: could not begin provider attempt");
2420
+ run.attempt.accountingAttempt = ctx.accounting?.startServe(run.resolvedAttempt, egressAt) ?? null;
2421
+ recordCredentialStarted(credentialWalk, credentialTrace, run.resolvedAttempt);
2422
+ tried.push(specOfTarget(run.target));
2423
+ },
2424
+ });
2425
+ };
2426
+ const primaryRun = beginAttemptRun(res, primaryOffer);
2427
+ let resolvedAttempt = primaryRun.resolvedAttempt;
2428
+ let target = primaryRun.target;
2429
+ let timer = primaryRun.timer;
2430
+ let onResClose = primaryRun.onResClose;
2431
+ let controller;
2432
+ let egressCallbackCalled;
2433
+ let hedged;
2434
+ let recoveryAudit;
2435
+ let attempt;
2436
+ let credentialRecorded = false;
2437
+ try {
2438
+ let forwardHeaders;
2303
2439
  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
- }
2440
+ forwardHeaders = buildForwardHeaders(ctx.inboundHeaders, resolvedAttempt);
2441
+ }
2442
+ catch (e) {
2443
+ credentialWalk.recordRejected(resolvedAttempt);
2444
+ if (e instanceof CredentialConfigError) {
2445
+ failClosed(res, 502, `llm-relay configuration: ${e.message}`);
2446
+ h.logger.write(baseLog(ctx.started, ctx.path, ctx.hadTools, false, 502, "skipped", null, attemptTrace.snapshot()));
2326
2447
  return;
2327
2448
  }
2449
+ throw e;
2328
2450
  }
2329
- catch (e) {
2330
- if (!attempt) {
2331
- if (credentialWalk.pending === resolvedAttempt) {
2332
- credentialWalk.recordRejected(resolvedAttempt);
2451
+ let upstream;
2452
+ {
2453
+ const raced = await runAttemptWithHedge({ run: primaryRun, promise: startAttempt(primaryRun, forwardHeaders) }, {
2454
+ h,
2455
+ res,
2456
+ walk: credentialWalk,
2457
+ credentialTrace,
2458
+ attemptTrace,
2459
+ tracker: pool429,
2460
+ startRun: (offer) => {
2461
+ const hedgeRun = beginAttemptRun(res, offer);
2462
+ try {
2463
+ return { run: hedgeRun, promise: startAttempt(hedgeRun, buildForwardHeaders(ctx.inboundHeaders, offer)) };
2464
+ }
2465
+ catch {
2466
+ releaseAttemptRun(res, hedgeRun);
2467
+ credentialWalk.recordRejected(offer);
2468
+ return undefined;
2469
+ }
2470
+ },
2471
+ });
2472
+ resolvedAttempt = raced.run.resolvedAttempt;
2473
+ target = raced.run.target;
2474
+ controller = raced.run.controller;
2475
+ timer = raced.run.timer;
2476
+ onResClose = raced.run.onResClose;
2477
+ attempt = raced.run.attempt;
2478
+ egressCallbackCalled = raced.run.egressCallbackCalled;
2479
+ hedged = raced.hedged;
2480
+ recoveryAudit = audits.get(raced.run) ?? { value: null };
2481
+ const settled = raced.settled;
2482
+ if (!settled.ok) {
2483
+ const e = settled.error;
2484
+ if (!attempt) {
2485
+ if (credentialWalk.isPending(resolvedAttempt)) {
2486
+ credentialWalk.recordRejected(resolvedAttempt);
2487
+ }
2488
+ if (res.destroyed)
2489
+ return;
2490
+ const status = controller.signal.aborted ? 504 : 502;
2491
+ failClosed(res, status, egressCallbackCalled
2492
+ ? "llm-relay: could not begin provider attempt"
2493
+ : "llm-relay: backend preparation failed");
2494
+ h.logger.write(baseLog(ctx.started, ctx.path, ctx.hadTools, false, status, "skipped", null, attemptTrace.snapshot()));
2495
+ return;
2333
2496
  }
2334
- if (res.destroyed)
2497
+ const aborted = controller.signal.aborted;
2498
+ const status = aborted ? 504 : 502;
2499
+ if (res.destroyed) {
2500
+ completeAttemptCancelled(h, attempt, "client disconnected");
2501
+ recordCredentialOutcome(credentialWalk, credentialTrace, resolvedAttempt, { kind: "cancelled" });
2502
+ credentialRecorded = true;
2335
2503
  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" });
2504
+ }
2505
+ completeAttemptFailure(h, attempt, {
2506
+ failure: "transport",
2507
+ provenance: aborted ? "deadline" : "upstream",
2508
+ status,
2509
+ });
2510
+ recordCredentialOutcome(credentialWalk, credentialTrace, resolvedAttempt, aborted ? { kind: "timeout" } : { kind: "provider-transport" });
2348
2511
  credentialRecorded = true;
2512
+ 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()), {
2513
+ kind: "transport",
2514
+ message: aborted ? "backend timed out" : `backend unreachable: ${e.message}`,
2515
+ servedBy: tried.join(", "),
2516
+ });
2517
+ if (walkEnd)
2518
+ continue;
2349
2519
  return;
2350
2520
  }
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;
2521
+ upstream = settled.value;
2522
+ }
2523
+ if (!attempt) {
2524
+ credentialWalk.recordRejected(resolvedAttempt);
2525
+ if (errorOrigin(upstream) === "local") {
2526
+ await forwardLocalResponse(res, upstream);
2527
+ h.logger.write(baseLog(ctx.started, ctx.path, ctx.hadTools, false, upstream.status, "skipped", null, attemptTrace.snapshot()));
2528
+ }
2529
+ else {
2530
+ await upstream.body?.cancel().catch(() => { });
2531
+ failClosed(res, 502, "llm-relay: backend returned before provider egress");
2532
+ h.logger.write(baseLog(ctx.started, ctx.path, ctx.hadTools, false, 502, "skipped", null, attemptTrace.snapshot()));
2533
+ }
2365
2534
  return;
2366
2535
  }
2367
2536
  const reportedModelSource = upstream;
@@ -2479,6 +2648,7 @@ async function openAiFrontPath(res, credentialWalk, credentialTrace, ctx, h) {
2479
2648
  degraded: degradedLabel(ctx.addressedPool ?? null, ctx.degradedSpecs ?? null, target),
2480
2649
  quotaDemoted: ctx.quotaDemotedFirst,
2481
2650
  latencyDemoted: ctx.latencyDemotedFirst,
2651
+ hedged,
2482
2652
  paid: ctx.cfg ? paidLabel(ctx.cfg, h, target) : null,
2483
2653
  sticky: ctx.sticky,
2484
2654
  });
@@ -2542,7 +2712,7 @@ async function openAiFrontPath(res, credentialWalk, credentialTrace, ctx, h) {
2542
2712
  recordCredentialOutcome(credentialWalk, credentialTrace, resolvedAttempt, res.destroyed ? { kind: "cancelled" } : { kind: "local" });
2543
2713
  credentialRecorded = true;
2544
2714
  }
2545
- else if (!credentialRecorded && credentialWalk.pending === resolvedAttempt) {
2715
+ else if (!credentialRecorded && credentialWalk.isPending(resolvedAttempt)) {
2546
2716
  credentialWalk.recordRejected(resolvedAttempt);
2547
2717
  }
2548
2718
  if (attempt && !attempt.completed) {
@@ -2592,6 +2762,8 @@ function responseHeadersForTarget(backendRes, ctx) {
2592
2762
  responseHeaders[QUOTA_DEMOTED_HEADER] = ctx.quotaDemoted;
2593
2763
  if (ctx.latencyDemoted)
2594
2764
  responseHeaders[LATENCY_DEMOTED_HEADER] = ctx.latencyDemoted;
2765
+ if (ctx.hedged)
2766
+ responseHeaders[HEDGED_HEADER] = ctx.hedged;
2595
2767
  if (ctx.paid)
2596
2768
  responseHeaders[PAID_HEADER] = ctx.paid;
2597
2769
  if (ctx.credentialHeaders)