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.
- package/dist/accounting-store-schema.d.ts +18 -0
- package/dist/accounting-store-schema.js +9 -2
- package/dist/accounting-store-schema.js.map +1 -1
- package/dist/accounting-store.js +4 -0
- package/dist/accounting-store.js.map +1 -1
- package/dist/accounting.d.ts +22 -0
- package/dist/accounting.js +6 -0
- package/dist/accounting.js.map +1 -1
- package/dist/backend.d.ts +23 -0
- package/dist/backend.js +1 -0
- package/dist/backend.js.map +1 -1
- package/dist/circuit-breaker.d.ts +14 -0
- package/dist/circuit-breaker.js +20 -1
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/cli.js +63 -48
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +48 -0
- package/dist/config.js +33 -0
- package/dist/config.js.map +1 -1
- package/dist/credential-select.d.ts +34 -0
- package/dist/credential-select.js +48 -26
- package/dist/credential-select.js.map +1 -1
- package/dist/dashboard-contract.d.ts +31 -0
- package/dist/dashboard-contract.js +14 -5
- package/dist/dashboard-contract.js.map +1 -1
- package/dist/dashboard-snapshot.js +3 -0
- package/dist/dashboard-snapshot.js.map +1 -1
- package/dist/hedge-race.d.ts +90 -0
- package/dist/hedge-race.js +48 -0
- package/dist/hedge-race.js.map +1 -0
- package/dist/hedge-trigger.d.ts +143 -0
- package/dist/hedge-trigger.js +55 -0
- package/dist/hedge-trigger.js.map +1 -0
- package/dist/kernel/contracts.d.ts +18 -0
- package/dist/ping/metrics.d.ts +22 -4
- package/dist/ping/metrics.js +20 -19
- package/dist/ping/metrics.js.map +1 -1
- package/dist/server.js +361 -179
- package/dist/server.js.map +1 -1
- 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
|
|
913
|
-
if (!
|
|
925
|
+
const primaryOffer = nextUncappedAttempt(h, credentialWalk, attemptTrace, pool429);
|
|
926
|
+
if (!primaryOffer)
|
|
914
927
|
break;
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
h
|
|
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
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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.
|
|
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
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
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
|
|
2204
|
-
if (!
|
|
2347
|
+
const primaryOffer = nextUncappedAttempt(h, credentialWalk, attemptTrace, pool429);
|
|
2348
|
+
if (!primaryOffer)
|
|
2205
2349
|
break;
|
|
2206
|
-
const
|
|
2207
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
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
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
: "
|
|
2340
|
-
|
|
2341
|
-
|
|
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
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
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.
|
|
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)
|