llm-relay 0.63.0 → 0.64.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 +16 -0
- package/dist/backend.js +1 -0
- package/dist/backend.js.map +1 -1
- package/dist/config.d.ts +30 -0
- package/dist/config.js +33 -0
- package/dist/config.js.map +1 -1
- package/dist/latency-demotion.d.ts +90 -0
- package/dist/latency-demotion.js +43 -0
- package/dist/latency-demotion.js.map +1 -0
- package/dist/mcp/server.d.ts +14 -0
- package/dist/mcp/server.js +16 -6
- package/dist/mcp/server.js.map +1 -1
- package/dist/server.js +35 -12
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/skills/llm-relay/SKILL.md +47 -6
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, 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, 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";
|
|
@@ -41,6 +41,7 @@ import { looksLikeContextLengthError, looksLikeMaxOutputError, parseStatedContex
|
|
|
41
41
|
import { looksLikeRateLimitError, parseStatedRateLimit, recordObservedRateLimit } from "./rate-limits.js";
|
|
42
42
|
import { clearFacts, cooldownUntil, factsFor, isCostBlocked, recordFact } from "./target-facts.js";
|
|
43
43
|
import { createQuotaDemotionFn, quotaDemotionLabel } from "./quota-demotion.js";
|
|
44
|
+
import { createLatencyDemotionFn, latencyDemotionLabel } from "./latency-demotion.js";
|
|
44
45
|
import { createHardCapLedgerReader, evaluateHardCap, hardCapLabel } from "./hard-cap.js";
|
|
45
46
|
import { applyResetRule, interpretRefusal, materializeScope, parseStatedResetMs, recordUnknownRefusal } from "./refusal-interpretation.js";
|
|
46
47
|
import { CONTROL_AUTHORIZATION_HEADER, createControlAuthorization, resolveControlAuthorizationConfigDir, validateControlAuthorization, } from "./control-authorization.js";
|
|
@@ -212,6 +213,10 @@ export function createProxy(cfg, deps = {}) {
|
|
|
212
213
|
? deps.accountingReader
|
|
213
214
|
: null,
|
|
214
215
|
});
|
|
216
|
+
const latencyDemotion = createLatencyDemotionFn({
|
|
217
|
+
breaker,
|
|
218
|
+
settings: cfg.routing?.latency,
|
|
219
|
+
});
|
|
215
220
|
const costClassOf = (attempt) => {
|
|
216
221
|
const t = attempt.target;
|
|
217
222
|
if (t.kind !== "openai" || !t.model)
|
|
@@ -323,6 +328,7 @@ export function createProxy(cfg, deps = {}) {
|
|
|
323
328
|
server,
|
|
324
329
|
...(controlAuthorization ? { controlAuthorization } : {}),
|
|
325
330
|
quotaDemotion,
|
|
331
|
+
latencyDemotion,
|
|
326
332
|
costClassOf,
|
|
327
333
|
hardCap: hardCapEvaluator,
|
|
328
334
|
}).catch((e) => {
|
|
@@ -781,7 +787,7 @@ async function handle(req, res, cfg, h) {
|
|
|
781
787
|
const rankedAttempts = rankCredentialAttempts(attempts, h.credentialLru, {
|
|
782
788
|
evidenceFor: (attempt) => credentialEvidence(attempt, cfg, h.breaker, routingNow),
|
|
783
789
|
});
|
|
784
|
-
const { ordered: orderedAttempts, quotaDemotedFirst } = orderDeploymentGroupsByUsability(rankedAttempts, h.breaker, routingNow, h.quotaDemotion, h.costClassOf);
|
|
790
|
+
const { ordered: orderedAttempts, quotaDemotedFirst, latencyDemotedFirst } = orderDeploymentGroupsByUsability(rankedAttempts, h.breaker, routingNow, h.quotaDemotion, h.costClassOf, h.latencyDemotion);
|
|
785
791
|
let walkAttempts = orderedAttempts;
|
|
786
792
|
let sticky = null;
|
|
787
793
|
if ((isMessages || openAiFrontProtocol) && h.stickySessions) {
|
|
@@ -854,6 +860,7 @@ async function handle(req, res, cfg, h) {
|
|
|
854
860
|
cfg,
|
|
855
861
|
accounting,
|
|
856
862
|
quotaDemotedFirst,
|
|
863
|
+
latencyDemotedFirst,
|
|
857
864
|
}, h);
|
|
858
865
|
return;
|
|
859
866
|
}
|
|
@@ -1098,6 +1105,7 @@ async function handle(req, res, cfg, h) {
|
|
|
1098
1105
|
tried,
|
|
1099
1106
|
degraded: degradedLabel(addressedPool, degradedSpecs, target),
|
|
1100
1107
|
quotaDemoted: quotaDemotedFirst,
|
|
1108
|
+
latencyDemoted: latencyDemotedFirst,
|
|
1101
1109
|
paid: paidLabel(cfg, h, target),
|
|
1102
1110
|
credentialHeaders: backendRes.status < 400
|
|
1103
1111
|
? credentialTrace.headers(resolvedAttempt)
|
|
@@ -1224,7 +1232,7 @@ function cooledByQuota(attempt, breaker, quotaDemotion, now) {
|
|
|
1224
1232
|
breaker.recordQuotaCooldown(targetIdentity(attempt), spent.resetsAt, now);
|
|
1225
1233
|
return true;
|
|
1226
1234
|
}
|
|
1227
|
-
function targetUsability(attempt, breaker, now, quotaDemotion, costClassOf) {
|
|
1235
|
+
function targetUsability(attempt, breaker, now, quotaDemotion, costClassOf, latencyDemotion) {
|
|
1228
1236
|
const identity = targetIdentity(attempt);
|
|
1229
1237
|
if (!breaker.isHealthy(identity, now))
|
|
1230
1238
|
return "cooling";
|
|
@@ -1232,6 +1240,8 @@ function targetUsability(attempt, breaker, now, quotaDemotion, costClassOf) {
|
|
|
1232
1240
|
return "cooling";
|
|
1233
1241
|
if (cooledByQuota(attempt, breaker, quotaDemotion, now))
|
|
1234
1242
|
return "cooling";
|
|
1243
|
+
if (latencyDemotion?.(attempt, now))
|
|
1244
|
+
return "cooling";
|
|
1235
1245
|
if (breaker.hasCredentialFault(identity, now))
|
|
1236
1246
|
return "credential-fault";
|
|
1237
1247
|
return "live";
|
|
@@ -1290,13 +1300,13 @@ function coolingLiftTime(attempt, breaker, now, costClassOf) {
|
|
|
1290
1300
|
}
|
|
1291
1301
|
return null;
|
|
1292
1302
|
}
|
|
1293
|
-
function orderDeploymentGroupsByUsability(attempts, breaker, now, quotaDemotion, costClassOf) {
|
|
1303
|
+
function orderDeploymentGroupsByUsability(attempts, breaker, now, quotaDemotion, costClassOf, latencyDemotion) {
|
|
1294
1304
|
const preferred = groupCredentialAttempts(attempts)[0]?.attempts[0];
|
|
1295
1305
|
const live = [];
|
|
1296
1306
|
const faulted = [];
|
|
1297
1307
|
const cooling = [];
|
|
1298
1308
|
for (const group of groupCredentialAttempts(attempts)) {
|
|
1299
|
-
const usability = targetUsability(group.attempts[0], breaker, now, quotaDemotion, costClassOf);
|
|
1309
|
+
const usability = targetUsability(group.attempts[0], breaker, now, quotaDemotion, costClassOf, latencyDemotion);
|
|
1300
1310
|
if (usability === "cooling")
|
|
1301
1311
|
cooling.push(group);
|
|
1302
1312
|
else if (usability === "credential-fault")
|
|
@@ -1316,6 +1326,16 @@ function orderDeploymentGroupsByUsability(attempts, breaker, now, quotaDemotion,
|
|
|
1316
1326
|
return liftA - liftB;
|
|
1317
1327
|
});
|
|
1318
1328
|
const ordered = [...live, ...faulted, ...cooling].flatMap((group) => group.attempts);
|
|
1329
|
+
let latencyDemotedFirst = null;
|
|
1330
|
+
if (preferred !== undefined &&
|
|
1331
|
+
latencyDemotion !== undefined &&
|
|
1332
|
+
latencyDemotion !== null &&
|
|
1333
|
+
ordered[0] !== undefined &&
|
|
1334
|
+
ordered[0] !== preferred) {
|
|
1335
|
+
const slow = latencyDemotion(preferred, now);
|
|
1336
|
+
if (slow)
|
|
1337
|
+
latencyDemotedFirst = latencyDemotionLabel(specOfTarget(preferred.target), slow);
|
|
1338
|
+
}
|
|
1319
1339
|
let quotaDemotedFirst = null;
|
|
1320
1340
|
if (preferred !== undefined &&
|
|
1321
1341
|
quotaDemotion !== undefined &&
|
|
@@ -1325,7 +1345,7 @@ function orderDeploymentGroupsByUsability(attempts, breaker, now, quotaDemotion,
|
|
|
1325
1345
|
quotaDemotion(preferred, now) !== null) {
|
|
1326
1346
|
quotaDemotedFirst = quotaDemotionLabel(specOfTarget(preferred.target), quotaDemotion(preferred, now));
|
|
1327
1347
|
}
|
|
1328
|
-
return { ordered, quotaDemotedFirst };
|
|
1348
|
+
return { ordered, quotaDemotedFirst, latencyDemotedFirst };
|
|
1329
1349
|
}
|
|
1330
1350
|
class CredentialAttemptTrace {
|
|
1331
1351
|
cfg;
|
|
@@ -1504,21 +1524,21 @@ function respondAllCapped(res, h, ctx, front, tracker, attempts) {
|
|
|
1504
1524
|
res.end(JSON.stringify(body));
|
|
1505
1525
|
h.logger.write(baseLog(ctx.started, ctx.path, ctx.hadTools, ctx.streamed, 429, "skipped", null, attempts));
|
|
1506
1526
|
}
|
|
1507
|
-
function applyStickyOrdering(ordered, pinnedSpec, breaker, degraded, now, quotaDemotion, costClassOf) {
|
|
1527
|
+
function applyStickyOrdering(ordered, pinnedSpec, breaker, degraded, now, quotaDemotion, costClassOf, latencyDemotion) {
|
|
1508
1528
|
const groups = groupCredentialAttempts(ordered);
|
|
1509
1529
|
const pinnedIndex = groups.findIndex((group) => specOfTarget(group.attempts[0].target) === pinnedSpec);
|
|
1510
1530
|
const pinnedGroup = pinnedIndex < 0 ? undefined : groups[pinnedIndex];
|
|
1511
1531
|
const pinned = pinnedGroup?.attempts[0];
|
|
1512
1532
|
if (!pinned || !pinnedGroup)
|
|
1513
1533
|
return { targets: ordered, status: "bypassed: not-in-pool" };
|
|
1514
|
-
const usability = targetUsability(pinned, breaker, now, quotaDemotion, costClassOf);
|
|
1534
|
+
const usability = targetUsability(pinned, breaker, now, quotaDemotion, costClassOf, latencyDemotion);
|
|
1515
1535
|
if (usability !== "live")
|
|
1516
1536
|
return { targets: ordered, status: `bypassed: ${usability}` };
|
|
1517
1537
|
if (degraded?.has(pinnedSpec)) {
|
|
1518
1538
|
const hasLiveInBand = groups.some((group) => {
|
|
1519
1539
|
const candidate = group.attempts[0];
|
|
1520
1540
|
return !degraded.has(specOfTarget(candidate.target)) &&
|
|
1521
|
-
targetUsability(candidate, breaker, now, quotaDemotion, costClassOf) === "live";
|
|
1541
|
+
targetUsability(candidate, breaker, now, quotaDemotion, costClassOf, latencyDemotion) === "live";
|
|
1522
1542
|
});
|
|
1523
1543
|
if (hasLiveInBand)
|
|
1524
1544
|
return { targets: ordered, status: "bypassed: degraded" };
|
|
@@ -1550,12 +1570,12 @@ export function orderByUsability(attempts, breaker = globalCircuitBreaker, now =
|
|
|
1550
1570
|
const { ordered } = orderByUsabilityTracked(attempts, breaker, now);
|
|
1551
1571
|
return ordered;
|
|
1552
1572
|
}
|
|
1553
|
-
function orderByUsabilityTracked(attempts, breaker, now, quotaDemotion, costClassOf) {
|
|
1573
|
+
function orderByUsabilityTracked(attempts, breaker, now, quotaDemotion, costClassOf, latencyDemotion) {
|
|
1554
1574
|
const live = [];
|
|
1555
1575
|
const faulted = [];
|
|
1556
1576
|
const cooling = [];
|
|
1557
1577
|
for (const attempt of attempts) {
|
|
1558
|
-
const usability = targetUsability(attempt, breaker, now, quotaDemotion, costClassOf);
|
|
1578
|
+
const usability = targetUsability(attempt, breaker, now, quotaDemotion, costClassOf, latencyDemotion);
|
|
1559
1579
|
if (usability === "cooling")
|
|
1560
1580
|
cooling.push(attempt);
|
|
1561
1581
|
else if (usability === "credential-fault")
|
|
@@ -1574,7 +1594,7 @@ function orderByUsabilityTracked(attempts, breaker, now, quotaDemotion, costClas
|
|
|
1574
1594
|
return -1;
|
|
1575
1595
|
return liftA - liftB;
|
|
1576
1596
|
});
|
|
1577
|
-
return { ordered: [...live, ...faulted, ...cooling], quotaDemotedFirst: null };
|
|
1597
|
+
return { ordered: [...live, ...faulted, ...cooling], quotaDemotedFirst: null, latencyDemotedFirst: null };
|
|
1578
1598
|
}
|
|
1579
1599
|
export function classifyStatus(status) {
|
|
1580
1600
|
if (status < 400)
|
|
@@ -2431,6 +2451,7 @@ async function openAiFrontPath(res, credentialWalk, credentialTrace, ctx, h) {
|
|
|
2431
2451
|
credentialHeaders: credentialTrace.headers(),
|
|
2432
2452
|
degraded: degradedLabel(ctx.addressedPool ?? null, ctx.degradedSpecs ?? null, target),
|
|
2433
2453
|
quotaDemoted: ctx.quotaDemotedFirst,
|
|
2454
|
+
latencyDemoted: ctx.latencyDemotedFirst,
|
|
2434
2455
|
paid: ctx.cfg ? paidLabel(ctx.cfg, h, target) : null,
|
|
2435
2456
|
sticky: ctx.sticky,
|
|
2436
2457
|
});
|
|
@@ -2542,6 +2563,8 @@ function responseHeadersForTarget(backendRes, ctx) {
|
|
|
2542
2563
|
responseHeaders[DEGRADED_HEADER] = ctx.degraded;
|
|
2543
2564
|
if (ctx.quotaDemoted)
|
|
2544
2565
|
responseHeaders[QUOTA_DEMOTED_HEADER] = ctx.quotaDemoted;
|
|
2566
|
+
if (ctx.latencyDemoted)
|
|
2567
|
+
responseHeaders[LATENCY_DEMOTED_HEADER] = ctx.latencyDemoted;
|
|
2545
2568
|
if (ctx.paid)
|
|
2546
2569
|
responseHeaders[PAID_HEADER] = ctx.paid;
|
|
2547
2570
|
if (ctx.credentialHeaders)
|