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/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)