llm-relay 0.80.0 → 0.82.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 (97) hide show
  1. package/dist/backend.d.ts +43 -3
  2. package/dist/backend.js +0 -0
  3. package/dist/backend.js.map +1 -1
  4. package/dist/candidate-runner.d.ts +45 -9
  5. package/dist/candidate-runner.js +55 -22
  6. package/dist/candidate-runner.js.map +1 -1
  7. package/dist/catalog.d.ts +49 -0
  8. package/dist/catalog.js +18 -0
  9. package/dist/catalog.js.map +1 -1
  10. package/dist/circuit-breaker.d.ts +118 -1
  11. package/dist/circuit-breaker.js +96 -1
  12. package/dist/circuit-breaker.js.map +1 -1
  13. package/dist/cli.d.ts +3 -0
  14. package/dist/cli.js +23 -3
  15. package/dist/cli.js.map +1 -1
  16. package/dist/config/routing-parser.d.ts +1 -1
  17. package/dist/config/routing-parser.js +27 -1
  18. package/dist/config/routing-parser.js.map +1 -1
  19. package/dist/config-edit.js +70 -16
  20. package/dist/config-edit.js.map +1 -1
  21. package/dist/config-types.d.ts +62 -6
  22. package/dist/config-types.js +1 -1
  23. package/dist/config-types.js.map +1 -1
  24. package/dist/dashboard/.vite/manifest.json +2 -2
  25. package/dist/dashboard/assets/index-B2UQHoym.js +74 -0
  26. package/dist/dashboard/assets/index-DFEP3XPr.css +1 -0
  27. package/dist/dashboard/index.html +2 -2
  28. package/dist/dispatch-lane-stats.d.ts +59 -5
  29. package/dist/dispatch-lane-stats.js +96 -29
  30. package/dist/dispatch-lane-stats.js.map +1 -1
  31. package/dist/dispatch.d.ts +143 -1
  32. package/dist/dispatch.js +260 -98
  33. package/dist/dispatch.js.map +1 -1
  34. package/dist/lane-affinity.d.ts +8 -0
  35. package/dist/lane-affinity.js +10 -0
  36. package/dist/lane-affinity.js.map +1 -1
  37. package/dist/log.d.ts +16 -0
  38. package/dist/log.js +1 -0
  39. package/dist/log.js.map +1 -1
  40. package/dist/mcp/agy-quota-log.d.ts +37 -0
  41. package/dist/mcp/agy-quota-log.js +68 -0
  42. package/dist/mcp/agy-quota-log.js.map +1 -0
  43. package/dist/mcp/job-archive.d.ts +55 -0
  44. package/dist/mcp/job-archive.js +151 -0
  45. package/dist/mcp/job-archive.js.map +1 -0
  46. package/dist/mcp/lane-runner.d.ts +108 -5
  47. package/dist/mcp/lane-runner.js +93 -4
  48. package/dist/mcp/lane-runner.js.map +1 -1
  49. package/dist/mcp/readonly-boundary.d.ts +46 -0
  50. package/dist/mcp/readonly-boundary.js +119 -0
  51. package/dist/mcp/readonly-boundary.js.map +1 -1
  52. package/dist/mcp/server.d.ts +132 -8
  53. package/dist/mcp/server.js +267 -68
  54. package/dist/mcp/server.js.map +1 -1
  55. package/dist/openai-request.d.ts +8 -0
  56. package/dist/openai-request.js +33 -9
  57. package/dist/openai-request.js.map +1 -1
  58. package/dist/pacing.d.ts +125 -0
  59. package/dist/pacing.js +83 -0
  60. package/dist/pacing.js.map +1 -0
  61. package/dist/ping/cadence.d.ts +57 -0
  62. package/dist/ping/cadence.js +58 -0
  63. package/dist/ping/cadence.js.map +1 -1
  64. package/dist/refusal-interpretation.d.ts +24 -0
  65. package/dist/refusal-interpretation.js +10 -1
  66. package/dist/refusal-interpretation.js.map +1 -1
  67. package/dist/responses-request.d.ts +14 -1
  68. package/dist/responses-request.js +21 -4
  69. package/dist/responses-request.js.map +1 -1
  70. package/dist/routes/admin.d.ts +8 -0
  71. package/dist/routes/admin.js +122 -35
  72. package/dist/routes/admin.js.map +1 -1
  73. package/dist/routes/messages.d.ts +2 -0
  74. package/dist/routes/messages.js +7 -3
  75. package/dist/routes/messages.js.map +1 -1
  76. package/dist/routes/openai-front.d.ts +1 -0
  77. package/dist/routes/openai-front.js +7 -3
  78. package/dist/routes/openai-front.js.map +1 -1
  79. package/dist/server.d.ts +4 -0
  80. package/dist/server.js +28 -4
  81. package/dist/server.js.map +1 -1
  82. package/dist/setup-claude.d.ts +2 -2
  83. package/dist/setup-claude.js +3 -3
  84. package/dist/stream-commit.d.ts +65 -0
  85. package/dist/stream-commit.js +150 -8
  86. package/dist/stream-commit.js.map +1 -1
  87. package/dist/target-facts.js +3 -0
  88. package/dist/target-facts.js.map +1 -1
  89. package/dist/telemetry.d.ts +9 -1
  90. package/dist/telemetry.js +1 -1
  91. package/dist/telemetry.js.map +1 -1
  92. package/package.json +1 -1
  93. package/scripts/install-skill.mjs +2 -2
  94. package/skills/llm-relay/SKILL.md +5 -3
  95. package/skills/llm-relay/references/dispatch-lanes.md +8 -2
  96. package/dist/dashboard/assets/index-Bqtx14ES.css +0 -1
  97. package/dist/dashboard/assets/index-CetZdLCA.js +0 -74
@@ -40,6 +40,7 @@ import { type QuotaObservation } from "./quota-observation.js";
40
40
  import { type FactResetBasis } from "./target-facts.js";
41
41
  import { type QuotaDemotionFn } from "./quota-demotion.js";
42
42
  import { type LatencyDemotionFn } from "./latency-demotion.js";
43
+ import { type PacingFn } from "./pacing.js";
43
44
  import type { HedgeDelayDecision } from "./hedge-trigger.js";
44
45
  import { type Settled } from "./hedge-race.js";
45
46
  import { type HardCapVerdict } from "./hard-cap.js";
@@ -62,7 +63,13 @@ export interface StickyRequestContext {
62
63
  /** The previously stored pin's routing evaluation; null means a new pin may be created. */
63
64
  provenance: string | null;
64
65
  }
65
- export type TargetUsability = "live" | "slow" | "credential-fault" | "cooling" | "probation";
66
+ /**
67
+ * The walk's health bands, best first: `probation` → `live` → `slow` → `paced` →
68
+ * `credential-fault` → `cooling`. `paced` (2026-09-15, `pacing.ts`) sits behind `slow` because a
69
+ * slow member most likely answers while one at its stated ceiling most likely 429s — and ahead of
70
+ * the failure bands because it is healthy, merely full for the moment.
71
+ */
72
+ export type TargetUsability = "live" | "slow" | "paced" | "credential-fault" | "cooling" | "probation";
66
73
  type OutcomeClass = "ok" | "retriable" | "credential" | "client";
67
74
  export declare const DEFAULT_WALK_BUDGET_MS = 45000;
68
75
  export declare const DEFAULT_STALL_TIMEOUT_MS = 90000;
@@ -80,6 +87,20 @@ export interface CandidateRunnerHandlers {
80
87
  * site that omits it silently disables the band there.
81
88
  */
82
89
  probation?: ProbationFn | null;
90
+ /**
91
+ * Evidence that a provider's own roster has moved: a 404 stating that a model THIS RELAY LISTS
92
+ * does not exist. The request path wires it to `ModelCatalog.noteProviderStale`, which re-fetches
93
+ * that provider's `/models` behind a per-provider cooldown.
94
+ *
95
+ * Optional, like `probation` and for the same reason — hand-built handler literals in tests keep
96
+ * compiling — and absent reads as "no trigger", which is the pre-existing behaviour: the catalog
97
+ * waits for its TTL. `createProxy` always sets it.
98
+ *
99
+ * ⚠ The handler is handed the attempt, not a provider name, so the containment the trigger needs
100
+ * — "on a model the catalog currently lists" — is decided where the catalog is, and not by a
101
+ * caller that would have to re-derive it.
102
+ */
103
+ catalogStale?: ((attempt: ResolvedAttempt) => void) | null;
83
104
  }
84
105
  interface ServedAnnouncementContext {
85
106
  readonly target: ResolvedTarget;
@@ -91,6 +112,7 @@ interface ServedAnnouncementContext {
91
112
  readonly degraded?: string | null | undefined;
92
113
  readonly quotaDemoted?: string | null | undefined;
93
114
  readonly latencyDemoted?: string | null | undefined;
115
+ readonly paced?: string | null | undefined;
94
116
  readonly probation?: string | null | undefined;
95
117
  readonly hedged?: string | null | undefined;
96
118
  readonly paid?: string | null | undefined;
@@ -174,7 +196,7 @@ export declare function probationLabel(spec: string, verdict: ProbationVerdict):
174
196
  export declare function probationLabelForAttempt(h: {
175
197
  probation?: ProbationFn | null;
176
198
  }, attempt: ResolvedAttempt, now: number): string | null;
177
- export declare function targetUsability(attempt: ResolvedAttempt, breaker: CircuitBreaker, now: number, quotaDemotion?: QuotaDemotionFn | null, costClassOf?: CostClassFn | null, latencyDemotion?: LatencyDemotionFn | null, probation?: ProbationFn | null): TargetUsability;
199
+ export declare function targetUsability(attempt: ResolvedAttempt, breaker: CircuitBreaker, now: number, quotaDemotion?: QuotaDemotionFn | null, costClassOf?: CostClassFn | null, latencyDemotion?: LatencyDemotionFn | null, probation?: ProbationFn | null, pacing?: PacingFn | null): TargetUsability;
178
200
  export declare function expandCredentialAttempts(targets: readonly ResolvedTarget[]): ResolvedAttempt[];
179
201
  export declare function credentialEvidence(attempt: ResolvedAttempt, cfg: Config, breaker: CircuitBreaker, now: number): {
180
202
  facts: {
@@ -191,10 +213,11 @@ export declare function credentialEvidence(attempt: ResolvedAttempt, cfg: Config
191
213
  quota: QuotaObservation[];
192
214
  cost: "free" | "unknown" | "paid";
193
215
  };
194
- export declare function orderDeploymentGroupsByUsability(attempts: readonly ResolvedAttempt[], breaker: CircuitBreaker, now: number, quotaDemotion?: QuotaDemotionFn | null, costClassOf?: CostClassFn | null, latencyDemotion?: LatencyDemotionFn | null, probation?: ProbationFn | null): {
216
+ export declare function orderDeploymentGroupsByUsability(attempts: readonly ResolvedAttempt[], breaker: CircuitBreaker, now: number, quotaDemotion?: QuotaDemotionFn | null, costClassOf?: CostClassFn | null, latencyDemotion?: LatencyDemotionFn | null, probation?: ProbationFn | null, pacing?: PacingFn | null): {
195
217
  ordered: ResolvedAttempt[];
196
218
  quotaDemotedFirst: string | null;
197
219
  latencyDemotedFirst: string | null;
220
+ pacedFirst: string | null;
198
221
  };
199
222
  export declare class CredentialAttemptTrace {
200
223
  private readonly cfg;
@@ -320,6 +343,12 @@ interface WalkExitData {
320
343
  errorOrigin?: ErrorOrigin;
321
344
  servedBy?: string;
322
345
  shouldTryNext?: boolean;
346
+ /**
347
+ * The dead stream's classified cause, when the backend stated one — see `stream-commit.ts`
348
+ * `stopCauseToken`. The WIRE half is already carried by `message`; this is the half that reaches
349
+ * the metadata log, and it is an enum-like token rather than any part of the backend's words.
350
+ */
351
+ streamStopCause?: string | undefined;
323
352
  }
324
353
  export declare function walkExitHeaders(tracker: Pool429Tracker, sticky: StickyRequestContext | null | undefined, credentialTrace: CredentialAttemptTrace, { errorOrigin, errorType, servedBy, }?: Pick<WalkExitData, "errorOrigin" | "errorType" | "servedBy">): Record<string, string>;
325
354
  export declare function endWalk(h: {
@@ -334,7 +363,7 @@ export declare function respondAllCapped(res: ServerResponse, h: {
334
363
  hadTools: boolean;
335
364
  streamed: boolean;
336
365
  }, front: "anthropic" | "openai", tracker: Pool429Tracker, attempts: readonly RequestAttemptLog[]): void;
337
- export declare function applyStickyOrdering(ordered: ResolvedAttempt[], pinnedSpec: string, breaker: CircuitBreaker, degraded: Set<string> | null, now: number, quotaDemotion?: QuotaDemotionFn | null, costClassOf?: CostClassFn | null, latencyDemotion?: LatencyDemotionFn | null, probation?: ProbationFn | null): {
366
+ export declare function applyStickyOrdering(ordered: ResolvedAttempt[], pinnedSpec: string, breaker: CircuitBreaker, degraded: Set<string> | null, now: number, quotaDemotion?: QuotaDemotionFn | null, costClassOf?: CostClassFn | null, latencyDemotion?: LatencyDemotionFn | null, probation?: ProbationFn | null, pacing?: PacingFn | null): {
338
367
  targets: ResolvedAttempt[];
339
368
  status: string;
340
369
  };
@@ -374,11 +403,12 @@ export declare function recordStickySuccess(h: {
374
403
  * hedged (`hedge-trigger.ts`: unmeasured IS hedged), so a probation member that hangs costs one
375
404
  * hedge, not a timeout — no second mechanism is added here.
376
405
  */
377
- export declare function orderByUsability(attempts: ResolvedAttempt[], breaker?: CircuitBreaker, now?: number, quotaDemotion?: QuotaDemotionFn | null, costClassOf?: CostClassFn | null, latencyDemotion?: LatencyDemotionFn | null, probation?: ProbationFn | null): ResolvedAttempt[];
378
- export declare function orderByUsabilityTracked(attempts: ResolvedAttempt[], breaker: CircuitBreaker, now: number, quotaDemotion?: QuotaDemotionFn | null, costClassOf?: CostClassFn | null, latencyDemotion?: LatencyDemotionFn | null, probation?: ProbationFn | null): {
406
+ export declare function orderByUsability(attempts: ResolvedAttempt[], breaker?: CircuitBreaker, now?: number, quotaDemotion?: QuotaDemotionFn | null, costClassOf?: CostClassFn | null, latencyDemotion?: LatencyDemotionFn | null, probation?: ProbationFn | null, pacing?: PacingFn | null): ResolvedAttempt[];
407
+ export declare function orderByUsabilityTracked(attempts: ResolvedAttempt[], breaker: CircuitBreaker, now: number, quotaDemotion?: QuotaDemotionFn | null, costClassOf?: CostClassFn | null, latencyDemotion?: LatencyDemotionFn | null, probation?: ProbationFn | null, pacing?: PacingFn | null): {
379
408
  ordered: ResolvedAttempt[];
380
409
  quotaDemotedFirst: string | null;
381
410
  latencyDemotedFirst: string | null;
411
+ pacedFirst: string | null;
382
412
  };
383
413
  /** What one HTTP status means to the walk: how to classify the outcome, and whether the body may
384
414
  * carry an eligibility fact worth interpreting. */
@@ -464,14 +494,20 @@ export interface HealthAttempt {
464
494
  terminal?: "succeeded" | "failed" | "cancelled";
465
495
  }
466
496
  export declare function targetIdentity(attempt: ResolvedAttempt): ProviderTargetIdentity;
497
+ /**
498
+ * Begin the breaker attempt for one egress. `estimatedInputTokens` is REQUIRED, not optional, so
499
+ * the compiler enumerates both fronts' call sites: it feeds the cell's attempt-start log that
500
+ * `pacing.ts` counts token windows over, and an optional parameter would let one front silently
501
+ * pace on requests alone — the one-front gap this repo's history warns about most.
502
+ */
467
503
  export declare function beginHealthAttempt(h: {
468
504
  breaker: CircuitBreaker;
469
- }, resolvedAttempt: ResolvedAttempt, started: number, trace: RequestAttemptTrace, usage: UsageAccumulator, accounting: RequestAccountingState | null): HealthAttempt | null;
505
+ }, resolvedAttempt: ResolvedAttempt, started: number, trace: RequestAttemptTrace, usage: UsageAccumulator, accounting: RequestAccountingState | null, estimatedInputTokens: number): HealthAttempt | null;
470
506
  type EligibilityObservation = {
471
507
  readonly unknown: boolean;
472
508
  readonly scope?: ReturnType<typeof materializeScope>;
473
509
  };
474
- export declare function observeEligibility(attempt: ResolvedAttempt, status: number, retryAfterMs: number | null, body: string): EligibilityObservation;
510
+ export declare function observeEligibility(attempt: ResolvedAttempt, status: number, retryAfterMs: number | null, body: string, h?: Pick<CandidateRunnerHandlers, "catalogStale">): EligibilityObservation;
475
511
  export declare function freeOnlyApplies(rule: {
476
512
  freeOnly?: boolean;
477
513
  }, rerouted: boolean): boolean;
@@ -485,7 +521,7 @@ type InspectedCandidateResponse = {
485
521
  response: Response;
486
522
  eligibility: EligibilityObservation;
487
523
  } | PostHeaderBodyFailure;
488
- export declare function inspectCandidateResponse(res: Response, attempt: ResolvedAttempt, retryAfterMs: number | null): Promise<InspectedCandidateResponse>;
524
+ export declare function inspectCandidateResponse(res: Response, attempt: ResolvedAttempt, retryAfterMs: number | null, h?: Pick<CandidateRunnerHandlers, "catalogStale">): Promise<InspectedCandidateResponse>;
489
525
  export declare function walkOutcomeForResponse(status: number, localFailure: boolean, scope?: EligibilityObservation["scope"]): CredentialWalkOutcome;
490
526
  export declare function observeAttemptHeaders(h: {
491
527
  breaker: CircuitBreaker;
@@ -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, PROBATION_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, PACED_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";
@@ -18,9 +18,10 @@ import { looksLikeRateLimitError, parseStatedRateLimit, recordObservedRateLimit
18
18
  import { clearFacts, cooldownUntil, factsFor, recordFact } from "./target-facts.js";
19
19
  import { quotaDemotionLabel } from "./quota-demotion.js";
20
20
  import { latencyDemotionLabel } from "./latency-demotion.js";
21
+ import { pacingLabel } from "./pacing.js";
21
22
  import { raceWithHedge } from "./hedge-race.js";
22
23
  import { hardCapLabel } from "./hard-cap.js";
23
- import { applyResetRule, interpretRefusal, materializeScope, parseStatedResetMs, recordUnknownRefusal, } from "./refusal-interpretation.js";
24
+ import { applyResetRule, interpretRefusal, materializeScope, parseStatedResetMs, recordUnknownRefusal, statesModelDoesNotExist, } from "./refusal-interpretation.js";
24
25
  import { DEFAULT_ANTHROPIC_VERSION } from "./config.js";
25
26
  import { buildAuthHeaders } from "./authEnv.js";
26
27
  import { CONTROL_AUTHORIZATION_HEADER } from "./control-authorization.js";
@@ -124,6 +125,8 @@ export function responseHeadersForTarget(backendRes, ctx) {
124
125
  responseHeaders[QUOTA_DEMOTED_HEADER] = ctx.quotaDemoted;
125
126
  if (ctx.latencyDemoted)
126
127
  responseHeaders[LATENCY_DEMOTED_HEADER] = ctx.latencyDemoted;
128
+ if (ctx.paced)
129
+ responseHeaders[PACED_HEADER] = ctx.paced;
127
130
  if (ctx.probation)
128
131
  responseHeaders[PROBATION_HEADER] = ctx.probation;
129
132
  if (ctx.hedged)
@@ -273,7 +276,7 @@ function cooledByQuota(attempt, breaker, quotaDemotion, now) {
273
276
  breaker.recordQuotaCooldown(targetIdentity(attempt), spent.resetsAt, now);
274
277
  return true;
275
278
  }
276
- export function targetUsability(attempt, breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation) {
279
+ export function targetUsability(attempt, breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation, pacing) {
277
280
  const identity = targetIdentity(attempt);
278
281
  if (!breaker.isHealthy(identity, now))
279
282
  return "cooling";
@@ -283,6 +286,8 @@ export function targetUsability(attempt, breaker, now, quotaDemotion, costClassO
283
286
  return "cooling";
284
287
  if (breaker.hasCredentialFault(identity, now))
285
288
  return "credential-fault";
289
+ if (pacing?.(attempt, now))
290
+ return "paced";
286
291
  if (latencyDemotion?.(attempt, now))
287
292
  return "slow";
288
293
  if (probation?.(attempt, now))
@@ -343,15 +348,16 @@ function coolingLiftTime(attempt, breaker, now, costClassOf) {
343
348
  }
344
349
  return null;
345
350
  }
346
- export function orderDeploymentGroupsByUsability(attempts, breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation) {
351
+ export function orderDeploymentGroupsByUsability(attempts, breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation, pacing) {
347
352
  const preferred = groupCredentialAttempts(attempts)[0]?.attempts[0];
348
353
  const probationGroups = [];
349
354
  const live = [];
350
355
  const slow = [];
356
+ const paced = [];
351
357
  const faulted = [];
352
358
  const cooling = [];
353
359
  for (const group of groupCredentialAttempts(attempts)) {
354
- const usability = targetUsability(group.attempts[0], breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation);
360
+ const usability = targetUsability(group.attempts[0], breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation, pacing);
355
361
  switch (usability) {
356
362
  case "probation":
357
363
  probationGroups.push(group);
@@ -362,6 +368,9 @@ export function orderDeploymentGroupsByUsability(attempts, breaker, now, quotaDe
362
368
  case "slow":
363
369
  slow.push(group);
364
370
  break;
371
+ case "paced":
372
+ paced.push(group);
373
+ break;
365
374
  case "credential-fault":
366
375
  faulted.push(group);
367
376
  break;
@@ -385,7 +394,17 @@ export function orderDeploymentGroupsByUsability(attempts, breaker, now, quotaDe
385
394
  return -1;
386
395
  return liftA - liftB;
387
396
  });
388
- const ordered = [...probationGroups, ...live, ...slow, ...faulted, ...cooling].flatMap((group) => group.attempts);
397
+ const ordered = [...probationGroups, ...live, ...slow, ...paced, ...faulted, ...cooling].flatMap((group) => group.attempts);
398
+ let pacedFirst = null;
399
+ if (preferred !== undefined &&
400
+ pacing !== undefined &&
401
+ pacing !== null &&
402
+ ordered[0] !== undefined &&
403
+ ordered[0] !== preferred) {
404
+ const pacedVerdict = pacing(preferred, now);
405
+ if (pacedVerdict)
406
+ pacedFirst = pacingLabel(specOfTarget(preferred.target), pacedVerdict);
407
+ }
389
408
  let latencyDemotedFirst = null;
390
409
  if (preferred !== undefined &&
391
410
  latencyDemotion !== undefined &&
@@ -405,7 +424,7 @@ export function orderDeploymentGroupsByUsability(attempts, breaker, now, quotaDe
405
424
  quotaDemotion(preferred, now) !== null) {
406
425
  quotaDemotedFirst = quotaDemotionLabel(specOfTarget(preferred.target), quotaDemotion(preferred, now));
407
426
  }
408
- return { ordered, quotaDemotedFirst, latencyDemotedFirst };
427
+ return { ordered, quotaDemotedFirst, latencyDemotedFirst, pacedFirst };
409
428
  }
410
429
  export class CredentialAttemptTrace {
411
430
  cfg;
@@ -705,7 +724,8 @@ export function endWalk(h, res, front, walk, attemptTrace, tracker, status, stic
705
724
  error: { message: exit.message, type: exit.errorType ?? "api_error" },
706
725
  }));
707
726
  }
708
- h.logger.write(log());
727
+ const record = log();
728
+ h.logger.write(exit.streamStopCause ? { ...record, streamStopCause: exit.streamStopCause } : record);
709
729
  return false;
710
730
  }
711
731
  export function respondAllCapped(res, h, ctx, front, tracker, attempts) {
@@ -735,21 +755,21 @@ export function respondAllCapped(res, h, ctx, front, tracker, attempts) {
735
755
  res.end(JSON.stringify(body));
736
756
  h.logger.write(baseLog(ctx.started, ctx.path, ctx.hadTools, ctx.streamed, 429, "skipped", null, attempts));
737
757
  }
738
- export function applyStickyOrdering(ordered, pinnedSpec, breaker, degraded, now, quotaDemotion, costClassOf, latencyDemotion, probation) {
758
+ export function applyStickyOrdering(ordered, pinnedSpec, breaker, degraded, now, quotaDemotion, costClassOf, latencyDemotion, probation, pacing) {
739
759
  const groups = groupCredentialAttempts(ordered);
740
760
  const pinnedIndex = groups.findIndex((group) => specOfTarget(group.attempts[0].target) === pinnedSpec);
741
761
  const pinnedGroup = pinnedIndex < 0 ? undefined : groups[pinnedIndex];
742
762
  const pinned = pinnedGroup?.attempts[0];
743
763
  if (!pinned || !pinnedGroup)
744
764
  return { targets: ordered, status: "bypassed: not-in-pool" };
745
- const usability = targetUsability(pinned, breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation);
765
+ const usability = targetUsability(pinned, breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation, pacing);
746
766
  if (usability !== "live")
747
767
  return { targets: ordered, status: `bypassed: ${usability}` };
748
768
  if (degraded?.has(pinnedSpec)) {
749
769
  const hasLiveInBand = groups.some((group) => {
750
770
  const candidate = group.attempts[0];
751
771
  return !degraded.has(specOfTarget(candidate.target)) &&
752
- targetUsability(candidate, breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation) === "live";
772
+ targetUsability(candidate, breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation, pacing) === "live";
753
773
  });
754
774
  if (hasLiveInBand)
755
775
  return { targets: ordered, status: "bypassed: degraded" };
@@ -777,18 +797,19 @@ export function recordStickySuccess(h, sticky, target, status) {
777
797
  return;
778
798
  h.stickySessions?.setPin(sticky.key, specOfTarget(target));
779
799
  }
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);
800
+ export function orderByUsability(attempts, breaker = globalCircuitBreaker, now = Date.now(), quotaDemotion, costClassOf, latencyDemotion, probation, pacing) {
801
+ const { ordered } = orderByUsabilityTracked(attempts, breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation, pacing);
782
802
  return ordered;
783
803
  }
784
- export function orderByUsabilityTracked(attempts, breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation) {
804
+ export function orderByUsabilityTracked(attempts, breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation, pacing) {
785
805
  const probationMembers = [];
786
806
  const live = [];
787
807
  const slow = [];
808
+ const paced = [];
788
809
  const faulted = [];
789
810
  const cooling = [];
790
811
  for (const attempt of attempts) {
791
- const usability = targetUsability(attempt, breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation);
812
+ const usability = targetUsability(attempt, breaker, now, quotaDemotion, costClassOf, latencyDemotion, probation, pacing);
792
813
  switch (usability) {
793
814
  case "probation":
794
815
  probationMembers.push(attempt);
@@ -799,6 +820,9 @@ export function orderByUsabilityTracked(attempts, breaker, now, quotaDemotion, c
799
820
  case "slow":
800
821
  slow.push(attempt);
801
822
  break;
823
+ case "paced":
824
+ paced.push(attempt);
825
+ break;
802
826
  case "credential-fault":
803
827
  faulted.push(attempt);
804
828
  break;
@@ -822,8 +846,8 @@ export function orderByUsabilityTracked(attempts, breaker, now, quotaDemotion, c
822
846
  return -1;
823
847
  return liftA - liftB;
824
848
  });
825
- const ordered = [...probationMembers, ...live, ...slow, ...faulted, ...cooling];
826
- return { ordered, quotaDemotedFirst: null, latencyDemotedFirst: null };
849
+ const ordered = [...probationMembers, ...live, ...slow, ...paced, ...faulted, ...cooling];
850
+ return { ordered, quotaDemotedFirst: null, latencyDemotedFirst: null, pacedFirst: null };
827
851
  }
828
852
  export const STATUS_VERDICT_TABLE = Object.freeze({
829
853
  400: { outcome: "retriable", carriesEligibilityFact: true },
@@ -1000,9 +1024,9 @@ export function targetIdentity(attempt) {
1000
1024
  base: target.base,
1001
1025
  });
1002
1026
  }
1003
- export function beginHealthAttempt(h, resolvedAttempt, started, trace, usage, accounting) {
1027
+ export function beginHealthAttempt(h, resolvedAttempt, started, trace, usage, accounting, estimatedInputTokens) {
1004
1028
  const identity = targetIdentity(resolvedAttempt);
1005
- const begun = h.breaker.beginAttempt(identity);
1029
+ const begun = h.breaker.beginAttempt(identity, { at: started, estimatedInputTokens });
1006
1030
  if (!begun.ok)
1007
1031
  return null;
1008
1032
  return {
@@ -1147,11 +1171,20 @@ function refusalBodyCandidates(body) {
1147
1171
  }
1148
1172
  return candidates;
1149
1173
  }
1150
- export function observeEligibility(attempt, status, retryAfterMs, body) {
1174
+ function statesModelDoesNotExistInAny(status, body) {
1175
+ for (const candidate of refusalBodyCandidates(body)) {
1176
+ if (statesModelDoesNotExist(status, candidate))
1177
+ return true;
1178
+ }
1179
+ return false;
1180
+ }
1181
+ export function observeEligibility(attempt, status, retryAfterMs, body, h) {
1151
1182
  const { target } = attempt;
1152
1183
  if (target.model === undefined)
1153
1184
  return { unknown: false };
1154
1185
  try {
1186
+ if (statesModelDoesNotExistInAny(status, body))
1187
+ h?.catalogStale?.(attempt);
1155
1188
  let matchedBody = body;
1156
1189
  let verdict = null;
1157
1190
  for (const candidate of refusalBodyCandidates(body)) {
@@ -1202,7 +1235,7 @@ export function resolveReset(interpretation, headerMs, body) {
1202
1235
  export function carriesEligibilityFact(status) {
1203
1236
  return statusVerdict(status).carriesEligibilityFact;
1204
1237
  }
1205
- export async function inspectCandidateResponse(res, attempt, retryAfterMs) {
1238
+ export async function inspectCandidateResponse(res, attempt, retryAfterMs, h) {
1206
1239
  const propagatedFailure = postHeaderBodyFailure(res);
1207
1240
  if (propagatedFailure)
1208
1241
  return propagatedFailure;
@@ -1222,7 +1255,7 @@ export async function inspectCandidateResponse(res, attempt, retryAfterMs) {
1222
1255
  observeMaxOutput(status, attempt.target, body);
1223
1256
  observeRateLimit(attempt, status, body);
1224
1257
  const eligibility = carriesEligibilityFact(status) && body
1225
- ? observeEligibility(attempt, status, retryAfterMs, body)
1258
+ ? observeEligibility(attempt, status, retryAfterMs, body, h)
1226
1259
  : { unknown: false };
1227
1260
  return {
1228
1261
  kind: "response",