llm-relay 0.57.0 → 0.58.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.
@@ -12,6 +12,10 @@
12
12
  * characters per token, and persisting it would put a guess into the one store whose whole value
13
13
  * is that it contains measurements. If nothing parses, nothing is learned. Same rule as
14
14
  * `resolveMetadata`: no rung may be a guess.
15
+ *
16
+ * The OUTPUT-token sibling lives here too (the `max-output` half below): a deployment that
17
+ * rejects an over-sized `max_tokens` often states its real output ceiling in the same kind of
18
+ * error body, under the same discipline.
15
19
  */
16
20
  /**
17
21
  * ⚠ **The storage moved; the PARSING is what this module is.** Ceilings now live in
@@ -48,7 +52,33 @@ export declare function observedContextLimit(provider: string, model: string, op
48
52
  path?: string;
49
53
  now?: number;
50
54
  }): number | null;
51
- /** Flush pending observations. Called on shutdown, like the other write-behind stores. */
55
+ /** Retained for callers and docs that name the output ceiling's staleness window (30 days). */
56
+ export declare const OBSERVED_MAX_OUTPUT_TTL_MS: number;
57
+ /**
58
+ * Extract a stated output-token ceiling from an error body, or null.
59
+ *
60
+ * Returns null for anything it cannot read as an explicit maximum — "max_tokens is too large"
61
+ * alone proves the request overshot but states no ceiling. Same fail-safe as
62
+ * `parseStatedContextLimit`: if nothing parses, nothing is learned.
63
+ */
64
+ export declare function parseStatedMaxOutput(body: string): number | null;
65
+ /** Does this error body describe a `max_tokens` / output-cap rejection at all? */
66
+ export declare function looksLikeMaxOutputError(body: string): boolean;
67
+ /**
68
+ * Record an output ceiling a deployment stated about itself. Deployment scope — the message names
69
+ * the model, never the account. A fresh observation replaces an older one, same as the context
70
+ * half: the deployment is the authority on its own ceiling.
71
+ */
72
+ export declare function recordObservedMaxOutput(provider: string, model: string, tokens: number, opts?: {
73
+ path?: string;
74
+ now?: number;
75
+ }): void;
76
+ /** The learned output ceiling for a deployment, or null when none was observed or it expired. */
77
+ export declare function observedMaxOutput(provider: string, model: string, opts?: {
78
+ path?: string;
79
+ now?: number;
80
+ }): number | null;
81
+ /** Flush pending observations — BOTH halves; the store is shared. Called on shutdown. */
52
82
  export declare function flushObservedContextLimits(opts?: {
53
83
  path?: string;
54
84
  }): void;
@@ -13,6 +13,10 @@ import { factsFor, flushFacts, recordFact, resetFacts, FACT_TTL_MS } from "./tar
13
13
  * characters per token, and persisting it would put a guess into the one store whose whole value
14
14
  * is that it contains measurements. If nothing parses, nothing is learned. Same rule as
15
15
  * `resolveMetadata`: no rung may be a guess.
16
+ *
17
+ * The OUTPUT-token sibling lives here too (the `max-output` half below): a deployment that
18
+ * rejects an over-sized `max_tokens` often states its real output ceiling in the same kind of
19
+ * error body, under the same discipline.
16
20
  */
17
21
  /**
18
22
  * ⚠ **The storage moved; the PARSING is what this module is.** Ceilings now live in
@@ -92,7 +96,88 @@ export function observedContextLimit(provider, model, opts = {}) {
92
96
  }
93
97
  return null;
94
98
  }
95
- /** Flush pending observations. Called on shutdown, like the other write-behind stores. */
99
+ // ── Max-output ceilings — the OUTPUT-token sibling of the context parser above ─────────────────
100
+ //
101
+ // The groq case that motivated this half: 13 requests answered 400 with "`max_tokens` must be
102
+ // less than or equal to `8192`…" and no store could hold the stated value, so the failure
103
+ // repeated on every walk that landed there. Same store, same TTL convention, same fail-safe:
104
+ // only an EXPLICITLY stated maximum is recorded, a miss learns nothing, and the fact is
105
+ // DISPLAY-ONLY — nothing clamps, refuses or routes on it
106
+ // (docs/max-output-caps-design-2026-08-29.md).
107
+ /** Retained for callers and docs that name the output ceiling's staleness window (30 days). */
108
+ export const OBSERVED_MAX_OUTPUT_TTL_MS = FACT_TTL_MS["max-output"];
109
+ /**
110
+ * Patterns that carry an explicitly stated `max_tokens` ceiling. Every pattern must capture the
111
+ * MAXIMUM, never the requested count — same rule as `STATED_LIMIT_PATTERNS`, for the same reason:
112
+ * the requested number is the larger one, and persisting it would overstate the real ceiling.
113
+ */
114
+ const STATED_MAX_OUTPUT_PATTERNS = [
115
+ // Groq: "`max_tokens` must be less than or equal to `8192`, the maximum value for `max_tokens`
116
+ // is less than the `context_window` for this model" — the ceiling is the number after the
117
+ // comparator, and the tail clause carries no number at all. Also TGI's "`max_new_tokens` must
118
+ // be <= 4096".
119
+ /max_(?:completion_|output_|new_)?tokens`?\s*must be (?:less than or equal to|<=)\s*`?(\d[\d,_]*)/i,
120
+ // OpenAI: "max_tokens is too large: 40000. This model supports at most 16384 completion tokens."
121
+ /supports?\s+at most\s+(\d[\d,_]*)\s+(?:completion|output)\s+tokens/i,
122
+ // Anthropic-style: "max_tokens: 40000 > 8192, which is the maximum allowed number of output
123
+ // tokens…" — anchored to the maximum wording, so a bare comparison never matches. Two fixed
124
+ // terminator forms rather than one optional-comma alternation: the gap class excludes ">" and
125
+ // each terminator starts unambiguously, which keeps the scan linear.
126
+ /max_tokens[^.\n>]{0,24}>\s*(\d[\d,_]*)\s+maximum\b/i,
127
+ /max_tokens[^.\n>]{0,24}>\s*(\d[\d,_]*),\s*which is the maximum/i,
128
+ // OpenAI-compatible front-ends that restate the field: "the maximum value for max_tokens is 8192".
129
+ /maximum value for\s+`?max_(?:completion_|output_|new_)?tokens`?\s*(?:is|:)\s*`?(\d[\d,_]*)/i,
130
+ ];
131
+ /**
132
+ * Extract a stated output-token ceiling from an error body, or null.
133
+ *
134
+ * Returns null for anything it cannot read as an explicit maximum — "max_tokens is too large"
135
+ * alone proves the request overshot but states no ceiling. Same fail-safe as
136
+ * `parseStatedContextLimit`: if nothing parses, nothing is learned.
137
+ */
138
+ export function parseStatedMaxOutput(body) {
139
+ if (typeof body !== "string" || body.length === 0)
140
+ return null;
141
+ // Bodies are small; this bound only stops a pathological one from driving the regex engine.
142
+ const text = body.length > 8192 ? body.slice(0, 8192) : body;
143
+ for (const re of STATED_MAX_OUTPUT_PATTERNS) {
144
+ const m = re.exec(text);
145
+ if (!m?.[1])
146
+ continue;
147
+ const n = Number(m[1].replace(/[,_]/g, ""));
148
+ if (Number.isFinite(n) && n > 0 && n <= MAX_CREDIBLE_TOKENS)
149
+ return Math.floor(n);
150
+ }
151
+ return null;
152
+ }
153
+ /** Does this error body describe a `max_tokens` / output-cap rejection at all? */
154
+ export function looksLikeMaxOutputError(body) {
155
+ return /max_(?:completion_|output_|new_)?tokens|(?:completion|output)\s+tokens/i.test(body);
156
+ }
157
+ /**
158
+ * Record an output ceiling a deployment stated about itself. Deployment scope — the message names
159
+ * the model, never the account. A fresh observation replaces an older one, same as the context
160
+ * half: the deployment is the authority on its own ceiling.
161
+ */
162
+ export function recordObservedMaxOutput(provider, model, tokens, opts = {}) {
163
+ if (!Number.isFinite(tokens) || tokens <= 0 || tokens > MAX_CREDIBLE_TOKENS)
164
+ return;
165
+ recordFact("max-output", { kind: "deployment", provider, model }, {
166
+ ...(opts.path !== undefined ? { path: opts.path } : {}),
167
+ ...(opts.now !== undefined ? { now: opts.now } : {}),
168
+ value: Math.floor(tokens),
169
+ });
170
+ }
171
+ /** The learned output ceiling for a deployment, or null when none was observed or it expired. */
172
+ export function observedMaxOutput(provider, model, opts = {}) {
173
+ // Output ceilings are deployment-scoped measurements, not credential-scoped facts.
174
+ for (const fact of factsFor(provider, null, model, opts)) {
175
+ if (fact.kind === "max-output" && typeof fact.value === "number")
176
+ return fact.value;
177
+ }
178
+ return null;
179
+ }
180
+ /** Flush pending observations — BOTH halves; the store is shared. Called on shutdown. */
96
181
  export function flushObservedContextLimits(opts = {}) {
97
182
  flushFacts(opts);
98
183
  }
@@ -1 +1 @@
1
- {"version":3,"file":"context-limits.js","sourceRoot":"","sources":["../src/context-limits.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"context-limits.js","sourceRoot":"","sources":["../src/context-limits.ts"],"names":[],"mappings":"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"}
package/dist/server.js CHANGED
@@ -35,7 +35,7 @@ import { specOfTarget } from "./benchmarks.js";
35
35
  import { extractQuotaObservations } from "./quota-observation.js";
36
36
  import { materializeDynamicPools } from "./dynamic-pools.js";
37
37
  import { baseLog } from "./request-log.js";
38
- import { looksLikeContextLengthError, parseStatedContextLimit, recordObservedContextLimit } from "./context-limits.js";
38
+ import { looksLikeContextLengthError, looksLikeMaxOutputError, parseStatedContextLimit, parseStatedMaxOutput, recordObservedContextLimit, recordObservedMaxOutput, } from "./context-limits.js";
39
39
  import { looksLikeRateLimitError, parseStatedRateLimit, recordObservedRateLimit } from "./rate-limits.js";
40
40
  import { clearFacts, cooldownUntil, factsFor, isCostBlocked, recordFact } from "./target-facts.js";
41
41
  import { createQuotaDemotionFn, quotaDemotionLabel } from "./quota-demotion.js";
@@ -1138,12 +1138,7 @@ async function handle(req, res, cfg, h) {
1138
1138
  const controller = new AbortController();
1139
1139
  const callerController = new AbortController();
1140
1140
  const timer = setTimeout(() => controller.abort(), target.timeoutMs);
1141
- const onResClose = () => {
1142
- if (!res.writableEnded) {
1143
- callerController.abort();
1144
- controller.abort();
1145
- }
1146
- };
1141
+ const onResClose = abortOnClientClose(res, callerController, controller);
1147
1142
  res.on("close", onResClose);
1148
1143
  let attempt;
1149
1144
  const usage = createUsageAccumulator();
@@ -2329,6 +2324,19 @@ function beginHealthAttempt(h, resolvedAttempt, started, trace, usage, accountin
2329
2324
  completed: false,
2330
2325
  };
2331
2326
  }
2327
+ /**
2328
+ * The candidate loops' client-gone reaction, shared by both fronts: when the client connection
2329
+ * closes before the response is finished, abort the caller-facing work and the in-flight backend
2330
+ * fetch together. One definition, so the two loops cannot drift.
2331
+ */
2332
+ function abortOnClientClose(res, callerController, controller) {
2333
+ return () => {
2334
+ if (!res.writableEnded) {
2335
+ callerController.abort();
2336
+ controller.abort();
2337
+ }
2338
+ };
2339
+ }
2332
2340
  /**
2333
2341
  * Learn a deployment's real context ceiling from an error it just returned.
2334
2342
  *
@@ -2360,6 +2368,37 @@ function observeContextLimit(status, target, body) {
2360
2368
  // Learning is best-effort and never in the request's way.
2361
2369
  }
2362
2370
  }
2371
+ /**
2372
+ * Learn a deployment's stated OUTPUT-token ceiling (`max_tokens`) from an error it just returned.
2373
+ *
2374
+ * ⚠ Called from BOTH request paths beside `observeContextLimit`, for the same reason that helper
2375
+ * documents: a learning loop wired into only one front would silently know nothing about half the
2376
+ * traffic (docs/pool-failover.md). Called with bytes already buffered by
2377
+ * `inspectCandidateResponse`, so it never tees or consumes the Response that may still become the
2378
+ * client's terminal real error.
2379
+ *
2380
+ * Display-only by owner decision (docs/max-output-caps-design-2026-08-29.md): the fact renders in
2381
+ * `llm-relay candidates`; nothing clamps, refuses, or routes on it.
2382
+ */
2383
+ function observeMaxOutput(status, target, body) {
2384
+ // A max_tokens rejection is a request-validation 400 (or a 413 variant); same gate as the
2385
+ // context observer beside it.
2386
+ if (status !== 400 && status !== 413)
2387
+ return;
2388
+ if (target.model === undefined)
2389
+ return;
2390
+ try {
2391
+ if (!looksLikeMaxOutputError(body))
2392
+ return;
2393
+ const stated = parseStatedMaxOutput(body);
2394
+ if (stated === null)
2395
+ return;
2396
+ recordObservedMaxOutput(target.provider, target.model, stated);
2397
+ }
2398
+ catch {
2399
+ // Learning is best-effort and never in the request's way.
2400
+ }
2401
+ }
2363
2402
  /**
2364
2403
  * Record the durable half of provider-stated quota observations as learned rate-limit facts.
2365
2404
  *
@@ -2625,6 +2664,7 @@ async function inspectCandidateResponse(res, attempt, retryAfterMs) {
2625
2664
  }
2626
2665
  const body = bytes.toString("utf8");
2627
2666
  observeContextLimit(status, attempt.target, body);
2667
+ observeMaxOutput(status, attempt.target, body);
2628
2668
  observeRateLimit(attempt, status, body);
2629
2669
  const eligibility = carriesEligibilityFact(status) && body
2630
2670
  ? observeEligibility(attempt, status, retryAfterMs, body)
@@ -2833,12 +2873,7 @@ async function openAiFrontPath(res, credentialWalk, credentialTrace, ctx, h) {
2833
2873
  const controller = new AbortController();
2834
2874
  const callerController = new AbortController();
2835
2875
  const timer = setTimeout(() => controller.abort(), target.timeoutMs);
2836
- const onResClose = () => {
2837
- if (!res.writableEnded) {
2838
- callerController.abort();
2839
- controller.abort();
2840
- }
2841
- };
2876
+ const onResClose = abortOnClientClose(res, callerController, controller);
2842
2877
  res.on("close", onResClose);
2843
2878
  let attempt;
2844
2879
  const usage = createUsageAccumulator();