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/backend.d.ts +23 -0
- package/dist/backend.js +1 -0
- package/dist/backend.js.map +1 -1
- package/dist/breaker-persistence.js +3 -5
- package/dist/breaker-persistence.js.map +1 -1
- package/dist/circuit-breaker.d.ts +41 -1
- package/dist/circuit-breaker.js +17 -2
- package/dist/circuit-breaker.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/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/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 +349 -177
- 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) => {
|
|
@@ -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
|
|
913
|
-
if (!
|
|
924
|
+
const primaryOffer = nextUncappedAttempt(h, credentialWalk, attemptTrace, pool429);
|
|
925
|
+
if (!primaryOffer)
|
|
914
926
|
break;
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
h
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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.
|
|
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
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
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
|
|
2204
|
-
if (!
|
|
2337
|
+
const primaryOffer = nextUncappedAttempt(h, credentialWalk, attemptTrace, pool429);
|
|
2338
|
+
if (!primaryOffer)
|
|
2205
2339
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" });
|
|
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
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
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.
|
|
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)
|