llm-relay 0.77.0 → 0.78.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.
Files changed (90) hide show
  1. package/dist/accounting-store-schema.js +5 -6
  2. package/dist/accounting-store-schema.js.map +1 -1
  3. package/dist/accounting-store.d.ts +10 -0
  4. package/dist/accounting-store.js +60 -6
  5. package/dist/accounting-store.js.map +1 -1
  6. package/dist/backend/envelope-validator.d.ts +1 -1
  7. package/dist/backend/envelope-validator.js +89 -52
  8. package/dist/backend/envelope-validator.js.map +1 -1
  9. package/dist/backend/health-prober.js +3 -1
  10. package/dist/backend/health-prober.js.map +1 -1
  11. package/dist/backend.d.ts +48 -0
  12. package/dist/backend.js +831 -7
  13. package/dist/backend.js.map +1 -1
  14. package/dist/candidate-runner.d.ts +135 -50
  15. package/dist/candidate-runner.js +160 -55
  16. package/dist/candidate-runner.js.map +1 -1
  17. package/dist/cli.d.ts +24 -2
  18. package/dist/cli.js +122 -20
  19. package/dist/cli.js.map +1 -1
  20. package/dist/config/routing-parser.d.ts +13 -8
  21. package/dist/config/routing-parser.js +369 -183
  22. package/dist/config/routing-parser.js.map +1 -1
  23. package/dist/config-types.d.ts +202 -0
  24. package/dist/config-types.js +2 -0
  25. package/dist/config-types.js.map +1 -1
  26. package/dist/config.d.ts +45 -1
  27. package/dist/config.js +87 -22
  28. package/dist/config.js.map +1 -1
  29. package/dist/dashboard-contract.d.ts +38 -0
  30. package/dist/dashboard-contract.js +19 -3
  31. package/dist/dashboard-contract.js.map +1 -1
  32. package/dist/dispatch-lane-stats.d.ts +33 -4
  33. package/dist/dispatch-lane-stats.js +65 -16
  34. package/dist/dispatch-lane-stats.js.map +1 -1
  35. package/dist/dispatch.d.ts +9 -0
  36. package/dist/dispatch.js +36 -4
  37. package/dist/dispatch.js.map +1 -1
  38. package/dist/key-checker.d.ts +21 -1
  39. package/dist/key-checker.js +38 -12
  40. package/dist/key-checker.js.map +1 -1
  41. package/dist/lane-affinity.d.ts +94 -1
  42. package/dist/lane-affinity.js +52 -4
  43. package/dist/lane-affinity.js.map +1 -1
  44. package/dist/mcp/lane-runner.d.ts +69 -16
  45. package/dist/mcp/lane-runner.js +30 -1
  46. package/dist/mcp/lane-runner.js.map +1 -1
  47. package/dist/mcp/server.d.ts +39 -4
  48. package/dist/mcp/server.js +70 -13
  49. package/dist/mcp/server.js.map +1 -1
  50. package/dist/openai-request.d.ts +21 -0
  51. package/dist/openai-request.js +1 -1
  52. package/dist/openai-request.js.map +1 -1
  53. package/dist/ping/cadence.js +9 -1
  54. package/dist/ping/cadence.js.map +1 -1
  55. package/dist/ping/probe-cache.d.ts +13 -0
  56. package/dist/ping/probe-cache.js +12 -0
  57. package/dist/ping/probe-cache.js.map +1 -1
  58. package/dist/responses-request.d.ts +20 -0
  59. package/dist/responses-request.js +7 -7
  60. package/dist/responses-request.js.map +1 -1
  61. package/dist/routes/admin.d.ts +8 -1
  62. package/dist/routes/admin.js +42 -6
  63. package/dist/routes/admin.js.map +1 -1
  64. package/dist/routes/messages.d.ts +1 -0
  65. package/dist/routes/messages.js +13 -8
  66. package/dist/routes/messages.js.map +1 -1
  67. package/dist/routes/openai-front.js +20 -7
  68. package/dist/routes/openai-front.js.map +1 -1
  69. package/dist/server.d.ts +9 -1
  70. package/dist/server.js +21 -3
  71. package/dist/server.js.map +1 -1
  72. package/dist/setup-claude.d.ts +2 -2
  73. package/dist/setup-claude.js +9 -7
  74. package/dist/setup-claude.js.map +1 -1
  75. package/dist/stream-pipeline.d.ts +116 -0
  76. package/dist/stream-pipeline.js +151 -0
  77. package/dist/stream-pipeline.js.map +1 -1
  78. package/dist/target-facts.d.ts +5 -4
  79. package/dist/target-facts.js +5 -0
  80. package/dist/target-facts.js.map +1 -1
  81. package/dist/telemetry.d.ts +15 -2
  82. package/dist/telemetry.js +4 -1
  83. package/dist/telemetry.js.map +1 -1
  84. package/dist/tier-data.d.ts +22 -0
  85. package/dist/tier-data.js +12 -2
  86. package/dist/tier-data.js.map +1 -1
  87. package/package.json +2 -1
  88. package/skills/llm-relay/SKILL.md +8 -3
  89. package/skills/llm-relay/references/dispatch-lanes.md +2 -1
  90. package/skills/llm-relay/references/operations.md +1 -1
@@ -1,5 +1,5 @@
1
1
  import { specOfTarget } from "./benchmarks.js";
2
- import { CREDENTIAL_HEADER, CREDENTIAL_ATTEMPTS_HEADER, POOL_ATTEMPTS_HEADER, HARD_CAP_HEADER, ERROR_ORIGIN_HEADER, SERVED_BY_HEADER, TOOL_DIALECT_HEADER, UNKNOWN_REFUSAL_HEADER, DEGRADED_HEADER, PAID_HEADER, QUOTA_DEMOTED_HEADER, LATENCY_DEMOTED_HEADER, HEDGED_HEADER, dialectRefusalSignalOf, errorOrigin, postHeaderBodyFailure, toolUseIdRewrites, toolCallIdRewrites, thoughtSignatureSentinels, upstreamReportedModel, } from "./backend.js";
2
+ import { CREDENTIAL_HEADER, CREDENTIAL_ATTEMPTS_HEADER, POOL_ATTEMPTS_HEADER, HARD_CAP_HEADER, ERROR_ORIGIN_HEADER, SERVED_BY_HEADER, TOOL_DIALECT_HEADER, UNKNOWN_REFUSAL_HEADER, DEGRADED_HEADER, PAID_HEADER, QUOTA_DEMOTED_HEADER, LATENCY_DEMOTED_HEADER, PROBATION_HEADER, HEDGED_HEADER, dialectRefusalSignalOf, errorOrigin, postHeaderBodyFailure, toolUseIdRewrites, toolCallIdRewrites, thoughtSignatureSentinels, upstreamReportedModel, } from "./backend.js";
3
3
  import { STICKY_PROVENANCE_HEADER } from "./session-pin.js";
4
4
  import { probeStreamForCommit } from "./stream-commit.js";
5
5
  import { DIALECT_REFUSED_DESTRUCTIVE_CODE } from "./tool-dialects.js";
@@ -25,15 +25,21 @@ import { DEFAULT_ANTHROPIC_VERSION } from "./config.js";
25
25
  import { buildAuthHeaders } from "./authEnv.js";
26
26
  import { CONTROL_AUTHORIZATION_HEADER } from "./control-authorization.js";
27
27
  import { STICKY_SESSION_HEADER } from "./session-pin.js";
28
- import { failClosed, HOP_BY_HOP } from "./stream-pipeline.js";
28
+ import { CrawlAbortedError, failClosed, HOP_BY_HOP } from "./stream-pipeline.js";
29
29
  const INTERNAL_REQUEST_HEADERS = new Set([
30
30
  "x-codex-turn-metadata",
31
31
  "x-llm-relay-dashboard-session",
32
32
  STICKY_SESSION_HEADER,
33
33
  CONTROL_AUTHORIZATION_HEADER,
34
34
  ]);
35
- const INBOUND_AUTH = ["authorization", "x-api-key"];
36
- export const MID_STREAM_ERROR_KIND = "backend_stream_failed";
35
+ const ALLOWED_FORWARD_HEADERS = new Set([
36
+ "content-type",
37
+ "accept",
38
+ "anthropic-version",
39
+ "anthropic-beta",
40
+ ]);
41
+ const MID_STREAM_ERROR_KIND = "backend_stream_failed";
42
+ const CRAWL_ERROR_KIND = "backend_stream_crawl";
37
43
  export function toolUseIdRewriteField(source) {
38
44
  const rewrites = toolUseIdRewrites(source);
39
45
  const outbound = toolCallIdRewrites(source);
@@ -52,7 +58,7 @@ export class CredentialConfigError extends Error {
52
58
  }
53
59
  export function buildForwardHeaders(inbound, attempt) {
54
60
  const { target, credential } = attempt;
55
- const stripAuth = credential.state !== "not-declared" || target.credentialMode === "contained";
61
+ const isContained = credential.state !== "not-declared" || target.credentialMode === "contained";
56
62
  const out = {};
57
63
  for (const [k, v] of Object.entries(inbound)) {
58
64
  const key = k.toLowerCase();
@@ -60,7 +66,7 @@ export function buildForwardHeaders(inbound, attempt) {
60
66
  continue;
61
67
  if (INTERNAL_REQUEST_HEADERS.has(key))
62
68
  continue;
63
- if (stripAuth && INBOUND_AUTH.includes(key))
69
+ if (isContained && !ALLOWED_FORWARD_HEADERS.has(key))
64
70
  continue;
65
71
  if (v === undefined)
66
72
  continue;
@@ -77,7 +83,7 @@ export function buildForwardHeaders(inbound, attempt) {
77
83
  export const DEFAULT_WALK_BUDGET_MS = 45_000;
78
84
  export const DEFAULT_STALL_TIMEOUT_MS = 90_000;
79
85
  const MAX_CAPPED_HEADER_CELLS = 5;
80
- export function filterResponseHeaders(hh) {
86
+ function filterResponseHeaders(hh) {
81
87
  const out = {};
82
88
  for (const [k, v] of hh.entries()) {
83
89
  const lower = k.toLowerCase();
@@ -118,6 +124,8 @@ export function responseHeadersForTarget(backendRes, ctx) {
118
124
  responseHeaders[QUOTA_DEMOTED_HEADER] = ctx.quotaDemoted;
119
125
  if (ctx.latencyDemoted)
120
126
  responseHeaders[LATENCY_DEMOTED_HEADER] = ctx.latencyDemoted;
127
+ if (ctx.probation)
128
+ responseHeaders[PROBATION_HEADER] = ctx.probation;
121
129
  if (ctx.hedged)
122
130
  responseHeaders[HEDGED_HEADER] = ctx.hedged;
123
131
  if (ctx.paid)
@@ -131,7 +139,7 @@ export function responseHeadersForTarget(backendRes, ctx) {
131
139
  }
132
140
  return responseHeaders;
133
141
  }
134
- export function endMidStreamFailure(res, errorFrame, message) {
142
+ function endMidStreamFailure(res, errorFrame, message) {
135
143
  if (res.writableEnded || res.destroyed)
136
144
  return;
137
145
  if (res.headersSent) {
@@ -143,32 +151,38 @@ export function endMidStreamFailure(res, errorFrame, message) {
143
151
  failClosed(res, 502, message);
144
152
  }
145
153
  }
146
- export function midStreamMessage(e) {
154
+ function midStreamMessage(e) {
147
155
  const errStr = e && typeof e === "object" && "message" in e && typeof e.message === "string"
148
156
  ? e.message
149
157
  : String(e);
150
158
  return `llm-relay: backend stream failed mid-response: ${errStr}`;
151
159
  }
152
- export function handleMidStreamError(res, e, started, path, hadTools, streamed, backendStatus, target, attempt, h, errorFrameBuilder, reportedModelSource, deadlineAborted = false, committed = false) {
153
- const message = midStreamMessage(e);
160
+ function crawlAbortReason(signal) {
161
+ return signal?.reason instanceof CrawlAbortedError ? signal.reason : undefined;
162
+ }
163
+ function completeMidStreamAttempt(h, res, attempt, deadlineAborted, committed) {
164
+ if (!attempt)
165
+ return;
166
+ if (res.destroyed) {
167
+ completeAttemptCancelled(h, attempt, "client disconnected");
168
+ return;
169
+ }
170
+ completeAttemptFailure(h, attempt, {
171
+ failure: deadlineAborted ? "transport" : "protocol",
172
+ provenance: deadlineAborted ? "deadline" : "upstream",
173
+ status: deadlineAborted ? 504 : 502,
174
+ ...(committed ? { logStatus: "committed" } : {}),
175
+ });
176
+ }
177
+ export function handleMidStreamError(res, e, started, path, hadTools, streamed, backendStatus, target, attempt, h, errorFrameBuilder, reportedModelSource, deadlineAborted = false, committed = false, signal) {
178
+ const crawl = crawlAbortReason(signal);
179
+ const message = crawl ? crawl.message : midStreamMessage(e);
154
180
  const errorFrame = errorFrameBuilder(message);
155
181
  endMidStreamFailure(res, errorFrame, message);
156
- if (attempt) {
157
- if (res.destroyed) {
158
- completeAttemptCancelled(h, attempt, "client disconnected");
159
- }
160
- else {
161
- completeAttemptFailure(h, attempt, {
162
- failure: deadlineAborted ? "transport" : "protocol",
163
- provenance: deadlineAborted ? "deadline" : "upstream",
164
- status: deadlineAborted ? 504 : 502,
165
- ...(committed ? { logStatus: "committed" } : {}),
166
- });
167
- }
168
- }
182
+ completeMidStreamAttempt(h, res, attempt, deadlineAborted, committed);
169
183
  h.logger.write({
170
184
  ...baseLog(started, path, hadTools, streamed, backendStatus, "skipped", target, attempt?.trace.snapshot(), reportedModelSource ? upstreamReportedModel(reportedModelSource) : undefined),
171
- errorKinds: [MID_STREAM_ERROR_KIND],
185
+ errorKinds: [crawl ? CRAWL_ERROR_KIND : MID_STREAM_ERROR_KIND],
172
186
  ...(reportedModelSource ? toolUseIdRewriteField(reportedModelSource) : {}),
173
187
  });
174
188
  }
@@ -186,7 +200,58 @@ export function degradedLabel(pool, degraded, target) {
186
200
  const spec = specOfTarget(target);
187
201
  return degraded.has(spec) ? `${spec} (below ${pool})` : null;
188
202
  }
189
- export function cooledByAllowance(attempt, now, costClass) {
203
+ export const DEFAULT_PROBATION_MIN_SAMPLES = 5;
204
+ export function resolveProbationSettings(settings) {
205
+ return {
206
+ enabled: settings?.enabled ?? true,
207
+ minSamples: settings?.minSamples ?? DEFAULT_PROBATION_MIN_SAMPLES,
208
+ };
209
+ }
210
+ export function resolveProbation(deps, attempt) {
211
+ const { enabled, minSamples } = resolveProbationSettings(deps.settings);
212
+ if (!enabled)
213
+ return null;
214
+ const model = attempt.target.model;
215
+ if (typeof model !== "string" || model.length === 0)
216
+ return null;
217
+ let cost;
218
+ try {
219
+ cost = deps.costClassOf?.(attempt);
220
+ }
221
+ catch {
222
+ return null;
223
+ }
224
+ if (cost !== "free")
225
+ return null;
226
+ let samples;
227
+ try {
228
+ samples = deps.readRequestSamples(attempt.target.provider, model);
229
+ }
230
+ catch {
231
+ return null;
232
+ }
233
+ if (!Number.isFinite(samples) || samples >= minSamples)
234
+ return null;
235
+ return { samples: Math.max(0, Math.floor(samples)), minSamples };
236
+ }
237
+ export function createProbationFn(deps) {
238
+ return (attempt) => {
239
+ try {
240
+ return resolveProbation(deps, attempt);
241
+ }
242
+ catch {
243
+ return null;
244
+ }
245
+ };
246
+ }
247
+ export function probationLabel(spec, verdict) {
248
+ return `${spec} (${verdict.samples} of ${verdict.minSamples} request samples)`;
249
+ }
250
+ export function probationLabelForAttempt(h, attempt, now) {
251
+ const verdict = h.probation?.(attempt, now) ?? null;
252
+ return verdict ? probationLabel(specOfTarget(attempt.target), verdict) : null;
253
+ }
254
+ function cooledByAllowance(attempt, now, costClass) {
190
255
  try {
191
256
  const { target } = attempt;
192
257
  const until = cooldownUntil(target.provider, attempt.credentialId, target.model ?? null, {
@@ -199,7 +264,7 @@ export function cooledByAllowance(attempt, now, costClass) {
199
264
  return false;
200
265
  }
201
266
  }
202
- export function cooledByQuota(attempt, breaker, quotaDemotion, now) {
267
+ function cooledByQuota(attempt, breaker, quotaDemotion, now) {
203
268
  if (!quotaDemotion)
204
269
  return false;
205
270
  const spent = quotaDemotion(attempt, now);
@@ -208,7 +273,7 @@ export function cooledByQuota(attempt, breaker, quotaDemotion, now) {
208
273
  breaker.recordQuotaCooldown(targetIdentity(attempt), spent.resetsAt, now);
209
274
  return true;
210
275
  }
211
- export function targetUsability(attempt, breaker, now, quotaDemotion, costClassOf, latencyDemotion) {
276
+ export function targetUsability(attempt, breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation) {
212
277
  const identity = targetIdentity(attempt);
213
278
  if (!breaker.isHealthy(identity, now))
214
279
  return "cooling";
@@ -220,6 +285,8 @@ export function targetUsability(attempt, breaker, now, quotaDemotion, costClassO
220
285
  return "credential-fault";
221
286
  if (latencyDemotion?.(attempt, now))
222
287
  return "slow";
288
+ if (probation?.(attempt, now))
289
+ return "probation";
223
290
  return "live";
224
291
  }
225
292
  export function expandCredentialAttempts(targets) {
@@ -260,7 +327,7 @@ export function credentialEvidence(attempt, cfg, breaker, now) {
260
327
  cost,
261
328
  };
262
329
  }
263
- export function coolingLiftTime(attempt, breaker, now, costClassOf) {
330
+ function coolingLiftTime(attempt, breaker, now, costClassOf) {
264
331
  const identity = targetIdentity(attempt);
265
332
  const breakerState = breaker.getState(identity);
266
333
  if (breakerState && breakerState.cooldownUntil > now) {
@@ -276,15 +343,19 @@ export function coolingLiftTime(attempt, breaker, now, costClassOf) {
276
343
  }
277
344
  return null;
278
345
  }
279
- export function orderDeploymentGroupsByUsability(attempts, breaker, now, quotaDemotion, costClassOf, latencyDemotion) {
346
+ export function orderDeploymentGroupsByUsability(attempts, breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation) {
280
347
  const preferred = groupCredentialAttempts(attempts)[0]?.attempts[0];
348
+ const probationGroups = [];
281
349
  const live = [];
282
350
  const slow = [];
283
351
  const faulted = [];
284
352
  const cooling = [];
285
353
  for (const group of groupCredentialAttempts(attempts)) {
286
- const usability = targetUsability(group.attempts[0], breaker, now, quotaDemotion, costClassOf, latencyDemotion);
354
+ const usability = targetUsability(group.attempts[0], breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation);
287
355
  switch (usability) {
356
+ case "probation":
357
+ probationGroups.push(group);
358
+ break;
288
359
  case "live":
289
360
  live.push(group);
290
361
  break;
@@ -314,7 +385,7 @@ export function orderDeploymentGroupsByUsability(attempts, breaker, now, quotaDe
314
385
  return -1;
315
386
  return liftA - liftB;
316
387
  });
317
- const ordered = [...live, ...slow, ...faulted, ...cooling].flatMap((group) => group.attempts);
388
+ const ordered = [...probationGroups, ...live, ...slow, ...faulted, ...cooling].flatMap((group) => group.attempts);
318
389
  let latencyDemotedFirst = null;
319
390
  if (preferred !== undefined &&
320
391
  latencyDemotion !== undefined &&
@@ -394,7 +465,7 @@ export class CredentialAttemptTrace {
394
465
  return headers;
395
466
  }
396
467
  }
397
- export function credentialAttemptLabel(outcome) {
468
+ function credentialAttemptLabel(outcome) {
398
469
  if (outcome?.status !== undefined)
399
470
  return outcome.status;
400
471
  if (outcome?.kind === "provider-transport")
@@ -433,7 +504,18 @@ export function nextUncappedAttempt(h, walk, attemptTrace, tracker) {
433
504
  walk.recordRejected(candidate);
434
505
  }
435
506
  }
436
- export function beginAttemptRun(res, offer) {
507
+ function firstByteFetch(fetchFn, onFirstByte) {
508
+ let notified = false;
509
+ return (input, init) => {
510
+ const p = fetchFn(input, init);
511
+ if (!notified) {
512
+ notified = true;
513
+ void p.then(onFirstByte, onFirstByte);
514
+ }
515
+ return p;
516
+ };
517
+ }
518
+ export function beginAttemptRun(res, offer, wantsStream) {
437
519
  const target = offer.target;
438
520
  const controller = new AbortController();
439
521
  const callerController = new AbortController();
@@ -447,18 +529,35 @@ export function beginAttemptRun(res, offer) {
447
529
  usage: createUsageAccumulator(),
448
530
  attempt: undefined,
449
531
  egressCallbackCalled: false,
532
+ fetchFn: fetch,
450
533
  };
534
+ const firstByteTimeoutMs = wantsStream ? undefined : target.firstByteTimeoutMs;
535
+ if (firstByteTimeoutMs !== undefined) {
536
+ run.firstByteTimeoutMs = firstByteTimeoutMs;
537
+ run.firstByteTimer = setTimeout(() => {
538
+ run.firstByteTimedOut = true;
539
+ controller.abort();
540
+ }, firstByteTimeoutMs);
541
+ run.fetchFn = firstByteFetch(fetch, () => {
542
+ if (run.firstByteTimer !== undefined) {
543
+ clearTimeout(run.firstByteTimer);
544
+ run.firstByteTimer = undefined;
545
+ }
546
+ });
547
+ }
451
548
  res.on("close", run.onResClose);
452
549
  return run;
453
550
  }
454
551
  export function releaseAttemptRun(res, run) {
455
552
  clearTimeout(run.timer);
553
+ if (run.firstByteTimer !== undefined)
554
+ clearTimeout(run.firstByteTimer);
456
555
  res.off("close", run.onResClose);
457
556
  }
458
- export function settleResponse(promise) {
557
+ function settleResponse(promise) {
459
558
  return promise.then((value) => ({ ok: true, value }), (error) => ({ ok: false, error }));
460
559
  }
461
- export function walkWouldFailOver(response) {
560
+ function walkWouldFailOver(response) {
462
561
  return errorOrigin(response) !== "local" && shouldTryNext(classifyStatus(response.status));
463
562
  }
464
563
  const COMMIT_PROBES = new WeakMap();
@@ -636,21 +735,21 @@ export function respondAllCapped(res, h, ctx, front, tracker, attempts) {
636
735
  res.end(JSON.stringify(body));
637
736
  h.logger.write(baseLog(ctx.started, ctx.path, ctx.hadTools, ctx.streamed, 429, "skipped", null, attempts));
638
737
  }
639
- export function applyStickyOrdering(ordered, pinnedSpec, breaker, degraded, now, quotaDemotion, costClassOf, latencyDemotion) {
738
+ export function applyStickyOrdering(ordered, pinnedSpec, breaker, degraded, now, quotaDemotion, costClassOf, latencyDemotion, probation) {
640
739
  const groups = groupCredentialAttempts(ordered);
641
740
  const pinnedIndex = groups.findIndex((group) => specOfTarget(group.attempts[0].target) === pinnedSpec);
642
741
  const pinnedGroup = pinnedIndex < 0 ? undefined : groups[pinnedIndex];
643
742
  const pinned = pinnedGroup?.attempts[0];
644
743
  if (!pinned || !pinnedGroup)
645
744
  return { targets: ordered, status: "bypassed: not-in-pool" };
646
- const usability = targetUsability(pinned, breaker, now, quotaDemotion, costClassOf, latencyDemotion);
745
+ const usability = targetUsability(pinned, breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation);
647
746
  if (usability !== "live")
648
747
  return { targets: ordered, status: `bypassed: ${usability}` };
649
748
  if (degraded?.has(pinnedSpec)) {
650
749
  const hasLiveInBand = groups.some((group) => {
651
750
  const candidate = group.attempts[0];
652
751
  return !degraded.has(specOfTarget(candidate.target)) &&
653
- targetUsability(candidate, breaker, now, quotaDemotion, costClassOf, latencyDemotion) === "live";
752
+ targetUsability(candidate, breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation) === "live";
654
753
  });
655
754
  if (hasLiveInBand)
656
755
  return { targets: ordered, status: "bypassed: degraded" };
@@ -661,7 +760,7 @@ export function applyStickyOrdering(ordered, pinnedSpec, breaker, degraded, now,
661
760
  .flatMap((group) => group.attempts);
662
761
  return { targets: reordered, status: "pinned, reordered" };
663
762
  }
664
- export function stickyHeaderValue(sticky, target, status) {
763
+ function stickyHeaderValue(sticky, target, status) {
665
764
  if (!sticky)
666
765
  return null;
667
766
  if (sticky.provenance)
@@ -678,18 +777,22 @@ export function recordStickySuccess(h, sticky, target, status) {
678
777
  return;
679
778
  h.stickySessions?.setPin(sticky.key, specOfTarget(target));
680
779
  }
681
- export function orderByUsability(attempts, breaker = globalCircuitBreaker, now = Date.now(), quotaDemotion, costClassOf, latencyDemotion) {
682
- const { ordered } = orderByUsabilityTracked(attempts, breaker, now, quotaDemotion, costClassOf, latencyDemotion);
780
+ export function orderByUsability(attempts, breaker = globalCircuitBreaker, now = Date.now(), quotaDemotion, costClassOf, latencyDemotion, probation) {
781
+ const { ordered } = orderByUsabilityTracked(attempts, breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation);
683
782
  return ordered;
684
783
  }
685
- export function orderByUsabilityTracked(attempts, breaker, now, quotaDemotion, costClassOf, latencyDemotion) {
784
+ export function orderByUsabilityTracked(attempts, breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation) {
785
+ const probationMembers = [];
686
786
  const live = [];
687
787
  const slow = [];
688
788
  const faulted = [];
689
789
  const cooling = [];
690
790
  for (const attempt of attempts) {
691
- const usability = targetUsability(attempt, breaker, now, quotaDemotion, costClassOf, latencyDemotion);
791
+ const usability = targetUsability(attempt, breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation);
692
792
  switch (usability) {
793
+ case "probation":
794
+ probationMembers.push(attempt);
795
+ break;
693
796
  case "live":
694
797
  live.push(attempt);
695
798
  break;
@@ -719,7 +822,8 @@ export function orderByUsabilityTracked(attempts, breaker, now, quotaDemotion, c
719
822
  return -1;
720
823
  return liftA - liftB;
721
824
  });
722
- return { ordered: [...live, ...slow, ...faulted, ...cooling], quotaDemotedFirst: null, latencyDemotedFirst: null };
825
+ const ordered = [...probationMembers, ...live, ...slow, ...faulted, ...cooling];
826
+ return { ordered, quotaDemotedFirst: null, latencyDemotedFirst: null };
723
827
  }
724
828
  export const STATUS_VERDICT_TABLE = Object.freeze({
725
829
  400: { outcome: "retriable", carriesEligibilityFact: true },
@@ -853,7 +957,7 @@ export class Pool429Tracker {
853
957
  return Math.min(this.minRetryAfterMs, finalRetryAfterMs ?? Infinity);
854
958
  }
855
959
  }
856
- export function recordCall(h, attempt, ok, completedAt) {
960
+ function recordCall(h, attempt, ok, completedAt) {
857
961
  const { target, usage } = attempt;
858
962
  if (!target.model || !h.modelCallRecorder)
859
963
  return;
@@ -915,7 +1019,7 @@ export function beginHealthAttempt(h, resolvedAttempt, started, trace, usage, ac
915
1019
  completed: false,
916
1020
  };
917
1021
  }
918
- export function abortOnClientClose(res, callerController, controller) {
1022
+ function abortOnClientClose(res, callerController, controller) {
919
1023
  return () => {
920
1024
  if (!res.writableEnded) {
921
1025
  callerController.abort();
@@ -923,7 +1027,7 @@ export function abortOnClientClose(res, callerController, controller) {
923
1027
  }
924
1028
  };
925
1029
  }
926
- export function observeContextLimit(status, target, body) {
1030
+ function observeContextLimit(status, target, body) {
927
1031
  if (status !== 400 && status !== 413)
928
1032
  return;
929
1033
  if (target.model === undefined)
@@ -939,7 +1043,7 @@ export function observeContextLimit(status, target, body) {
939
1043
  catch {
940
1044
  }
941
1045
  }
942
- export function observeMaxOutput(status, target, body) {
1046
+ function observeMaxOutput(status, target, body) {
943
1047
  if (status !== 400 && status !== 413)
944
1048
  return;
945
1049
  if (target.model === undefined)
@@ -955,7 +1059,7 @@ export function observeMaxOutput(status, target, body) {
955
1059
  catch {
956
1060
  }
957
1061
  }
958
- export function observeStatedRateLimits(attempt, observations) {
1062
+ function observeStatedRateLimits(attempt, observations) {
959
1063
  if (observations.length === 0)
960
1064
  return;
961
1065
  const { target } = attempt;
@@ -975,7 +1079,7 @@ export function observeStatedRateLimits(attempt, observations) {
975
1079
  catch {
976
1080
  }
977
1081
  }
978
- export function observeRateLimit(attempt, status, body) {
1082
+ function observeRateLimit(attempt, status, body) {
979
1083
  if (status !== 429)
980
1084
  return;
981
1085
  if (attempt.target.model === undefined)
@@ -991,7 +1095,7 @@ export function observeRateLimit(attempt, status, body) {
991
1095
  catch {
992
1096
  }
993
1097
  }
994
- export function refusalBodyCandidates(body) {
1098
+ function refusalBodyCandidates(body) {
995
1099
  const candidates = [];
996
1100
  const queued = [body];
997
1101
  const seen = new Set();
@@ -1196,6 +1300,7 @@ export function markAttemptCommitted(attempt) {
1196
1300
  export function completeAttemptSuccess(h, attempt, status) {
1197
1301
  if (attempt.completed)
1198
1302
  return;
1303
+ const costClass = h.costClassOf?.(attempt.resolvedAttempt);
1199
1304
  const completedAt = Date.now();
1200
1305
  const result = h.breaker.completeAttempt(attempt.handle, {
1201
1306
  terminal: "succeeded",
@@ -1213,7 +1318,7 @@ export function completeAttemptSuccess(h, attempt, status) {
1213
1318
  recordCall(h, attempt, true, completedAt);
1214
1319
  attempt.accounting?.complete(attempt.accountingAttempt, "success", null, attempt.usage, completedAt);
1215
1320
  try {
1216
- const cleared = clearFacts(attempt.target.provider, attempt.resolvedAttempt.credentialId, attempt.target.model ?? null);
1321
+ const cleared = clearFacts(attempt.target.provider, attempt.resolvedAttempt.credentialId, attempt.target.model ?? null, { costClass });
1217
1322
  if (cleared.includes("credential-invalid")) {
1218
1323
  h.breaker.clearCredentialFaults(attempt.resolvedAttempt.credentialId);
1219
1324
  }
@@ -1246,10 +1351,10 @@ export function completeAttemptFailure(h, attempt, options) {
1246
1351
  export function completeAttemptCancelled(h, attempt, reason) {
1247
1352
  completeCancellation(h, attempt, reason, attempt.committed ? "client-gone-mid-response" : "client-gone-before-response");
1248
1353
  }
1249
- export function completeAttemptAbandoned(h, attempt, reason) {
1354
+ function completeAttemptAbandoned(h, attempt, reason) {
1250
1355
  completeCancellation(h, attempt, reason, "relay-abandoned");
1251
1356
  }
1252
- export function completeCancellation(h, attempt, reason, cause) {
1357
+ function completeCancellation(h, attempt, reason, cause) {
1253
1358
  if (attempt.completed)
1254
1359
  return;
1255
1360
  const completedAt = Date.now();