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.
- package/dist/candidates.d.ts +2 -2
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -1
- package/dist/context-limits.d.ts +31 -1
- package/dist/context-limits.js +86 -1
- package/dist/context-limits.js.map +1 -1
- package/dist/server.js +48 -13
- package/dist/server.js.map +1 -1
- package/dist/target-facts.d.ts +6 -5
- package/dist/target-facts.js +5 -3
- package/dist/target-facts.js.map +1 -1
- package/package.json +1 -1
package/dist/context-limits.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|
package/dist/context-limits.js
CHANGED
|
@@ -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
|
-
|
|
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":"AAAA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAE9F
|
|
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();
|