llm-relay 0.22.0 → 0.24.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/metadata.js CHANGED
@@ -111,8 +111,12 @@ export function estimateRequestTokens(reqJson) {
111
111
  * Two rungs, both REAL PUBLICATIONS — there is deliberately no guessed rung, for the same reason
112
112
  * `resolveMetadata` has none and the request-path guardrail stays silent on an unknown limit:
113
113
  *
114
- * 1. `provider` — the serving deployment's own published `contextLength`. Authoritative.
115
- * 2. `snapshot` — `context_length` from the synced capability snapshot (`docs/tier-data.json`,
114
+ * 1. `observed` — a ceiling THIS deployment stated when it refused an over-length request. The
115
+ * strongest evidence there is: a first-party fact about the exact deployment that will serve
116
+ * the next request, which a published catalogue figure can contradict by being generic or
117
+ * stale. Only an explicitly stated maximum is ever recorded — see `context-limits.ts`.
118
+ * 2. `provider` — the serving deployment's own published `contextLength`.
119
+ * 3. `snapshot` — `context_length` from the synced capability snapshot (`docs/tier-data.json`,
116
120
  * OpenRouter), matched **exactly** on the spec's last segment.
117
121
  *
118
122
  * ⚠ Rung 2 exists because rung 1 is nearly empty in practice: free providers publish little
@@ -127,12 +131,17 @@ export function estimateRequestTokens(reqJson) {
127
131
  *
128
132
  * The provider limit lookup is injected so this module keeps no catalog dependency.
129
133
  */
130
- export function contextWindowResolver(providerLimit, snapshot) {
134
+ export function contextWindowResolver(providerLimit, snapshot, observed) {
131
135
  const positive = (n) => typeof n === "number" && Number.isFinite(n) && n > 0;
132
136
  return (spec) => {
133
137
  const slash = spec.indexOf("/");
134
138
  if (slash > 0) {
135
- const own = providerLimit(spec.slice(0, slash), spec.slice(slash + 1));
139
+ const provider = spec.slice(0, slash);
140
+ const model = spec.slice(slash + 1);
141
+ const learned = observed?.(provider, model) ?? null;
142
+ if (positive(learned))
143
+ return { tokens: learned, source: "observed" };
144
+ const own = providerLimit(provider, model);
136
145
  if (positive(own))
137
146
  return { tokens: own, source: "provider" };
138
147
  }
@@ -1 +1 @@
1
- {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,183 @@
1
+ import type { EligibilityClass, EligibilityScope } from "./deployment-eligibility.js";
2
+ /**
3
+ * What a backend's refusal MEANS — a lookup, never an inference, on the request path.
4
+ *
5
+ * The problem this solves: a refusal's status code does not carry its meaning. A 403 is at least
6
+ * four different facts — a revoked key, a plan-gated model, a license-gated model, a policy
7
+ * refusal — and only the message distinguishes them, in wording each vendor invents for itself.
8
+ * A hand-written pattern set can only ever cover the messages its author happened to see; every
9
+ * other refusal teaches nothing, forever, and is re-discovered on every single request.
10
+ *
11
+ * The shape, and the boundary it respects:
12
+ *
13
+ * REQUEST PATH — deterministic lookup, and nothing else. A refusal is reduced to a SIGNATURE
14
+ * (provider + model + the message with its variable parts stripped) and looked up in the
15
+ * confirmed table. A hit applies. A miss learns NOTHING about the deployment — exactly the
16
+ * fail-safe that already governs `context-limits.ts` — and records the signature as unseen.
17
+ *
18
+ * OUT OF BAND — judgement is allowed here and nowhere else. `llm-relay eligibility` surfaces
19
+ * unseen signatures with their samples; an agent researches what that message means for that
20
+ * provider, that model, this account; the verdict is written to the table and takes effect only
21
+ * once accepted.
22
+ *
23
+ * ⚠ **An LLM never decides a live routing decision here.** CLAUDE.md's repair boundary — "routing
24
+ * decisions come from config and deterministic classification, never from an LLM's opinion
25
+ * inserted into the request path" — is why the research tier is offline and why its output must be
26
+ * accepted before it binds. The relationship is the same one `docs/tier-data.json` already has to
27
+ * pool ranking: a model may author the data, the request path only ever reads it.
28
+ *
29
+ * ⚠ **Signatures are keyed per (provider, model, message).** The same provider sends different
30
+ * wording for different models, and the same wording can mean different things for a model the
31
+ * account has access to and one it does not. Keying on the message alone would let a verdict
32
+ * researched for one SKU evict a sibling that works. A new model showing a known message is a
33
+ * MISS until researched — conservative, which is the safe direction: the cost of a miss is one
34
+ * wasted round-trip, the cost of a false hit is a working deployment evicted from every pool.
35
+ */
36
+ /** A refusal interpretation, however it got here. */
37
+ export interface Interpretation {
38
+ class: EligibilityClass;
39
+ scope: EligibilityScope;
40
+ /**
41
+ * `seed` — shipped in this file, derived from first-party probes and reviewable in source.
42
+ * Binds immediately: it is deterministic code, not an opinion.
43
+ * `researched` — proposed out of band. Does NOT bind until accepted.
44
+ */
45
+ source: "seed" | "researched";
46
+ /** Epoch ms an operator (or an agent acting for one) accepted a researched verdict. */
47
+ acceptedAt?: number;
48
+ /** Why — free text from the research step, shown at review time. */
49
+ rationale?: string;
50
+ }
51
+ /** A refusal whose meaning is not known, held for the offline research tier. */
52
+ export interface UnknownRefusal {
53
+ provider: string;
54
+ model: string | null;
55
+ status: number;
56
+ /** The normalized message — the signature's readable half. */
57
+ normalized: string;
58
+ /** One verbatim sample, so a researcher sees what was actually said. Truncated. */
59
+ sample: string;
60
+ count: number;
61
+ firstSeen: number;
62
+ lastSeen: number;
63
+ /** A researched verdict awaiting acceptance. Present once the research tier has run. */
64
+ proposed?: {
65
+ class: EligibilityClass;
66
+ scope: EligibilityScope;
67
+ rationale: string;
68
+ at: number;
69
+ };
70
+ }
71
+ /**
72
+ * Reduce a refusal message to its invariant shape.
73
+ *
74
+ * Everything that varies between two occurrences of the SAME refusal has to go, or the table gets
75
+ * one entry per request: uuids and account ids (NIM names both), numbers (credit balances, token
76
+ * counts) and urls (upgrade links carry per-request refs).
77
+ *
78
+ * ⚠ **The message text itself must survive.** An earlier version also replaced every quoted string
79
+ * with a placeholder, on the theory that quoted model names are noise. Against a live pool that
80
+ * erased the entire payload: the Anthropic front hands this the relay's own wrapper —
81
+ * `openai backend HTTP 402: {"error":"You have depleted your monthly included credits…"}` — whose
82
+ * message is *inside* quotes, so all 15 members of a real `pool/xhigh` normalized to
83
+ * `openai backend http <n>: {<name>:<name>}` and nothing could ever match. Quoted model names are
84
+ * harmless: the model is already part of the key.
85
+ *
86
+ * ⚠ For the same reason the JSON hunt digs through a WRAPPER. A body is not always JSON at the top
87
+ * level — the relay's own error envelope prefixes it with prose — so a failed parse falls back to
88
+ * the first embedded object rather than giving up.
89
+ *
90
+ * The result is kept as readable TEXT rather than a hash: a human or an agent has to read these to
91
+ * research them, and a hex digest would make the store unreviewable for no benefit.
92
+ */
93
+ export declare function normalizeRefusalMessage(body: string): string;
94
+ /** The lookup key. Per (provider, model, normalized message) — see the header for why all three. */
95
+ export declare function refusalSignature(provider: string, model: string | null | undefined, status: number, body: string): string;
96
+ /**
97
+ * Interpretations shipped with the relay, each derived from a refusal observed first-party against
98
+ * a real account (probed 2026-08-08). These bind without review: they are deterministic code in
99
+ * version control, and an operator who disagrees can override the entry.
100
+ *
101
+ * ⚠ These are matched as PATTERNS against the normalized message, not as exact signatures, because
102
+ * a seed has to cover a provider it has never been run against. That is a deliberate exception to
103
+ * the per-model keying rule above and the only one — a seed is reviewed source, whereas a
104
+ * researched verdict is a model's opinion and gets the conservative key.
105
+ *
106
+ * ⚠ **`allowance-exhausted` is not a cost verdict.** See `deployment-eligibility.ts` — it means the
107
+ * deployment is free and currently spent, and it must never evict anything from a free pool.
108
+ */
109
+ export declare const SEED_INTERPRETATIONS: Array<{
110
+ status: (s: number) => boolean;
111
+ pattern: RegExp;
112
+ class: EligibilityClass;
113
+ scope: EligibilityScope;
114
+ note: string;
115
+ }>;
116
+ /**
117
+ * What this refusal means, or null when nothing confirmed covers it.
118
+ *
119
+ * Order: the operator-visible table first (so an override beats a seed), then the seeds. A miss
120
+ * returns null and the caller must learn nothing — that is the whole fail-safe.
121
+ */
122
+ export declare function interpretRefusal(provider: string, model: string | null | undefined, status: number, body: string, opts?: {
123
+ path?: string;
124
+ }): Interpretation | null;
125
+ /**
126
+ * Hold an uninterpretable refusal for the research tier.
127
+ *
128
+ * Deliberately cheap and lossy: one verbatim sample per signature and a count. The queue exists to
129
+ * tell a researcher "this message happens, here is what it looks like", not to be a log — the
130
+ * metadata logger already covers the traffic, and it is metadata-only precisely so bodies do not
131
+ * land on disk. This file holds error bodies, which are the provider's own text about a
132
+ * deployment, never the user's prompt.
133
+ */
134
+ export declare function recordUnknownRefusal(provider: string, model: string | null | undefined, status: number, body: string, opts?: {
135
+ path?: string;
136
+ now?: number;
137
+ }): void;
138
+ /** Unseen refusals, most-frequent first — the research tier's work list. */
139
+ export declare function pendingRefusals(opts?: {
140
+ path?: string;
141
+ }): Array<UnknownRefusal & {
142
+ signature: string;
143
+ }>;
144
+ /** Attach a researched verdict to a pending signature. It does NOT bind until accepted. */
145
+ export declare function proposeInterpretation(signature: string, proposal: {
146
+ class: EligibilityClass;
147
+ scope: EligibilityScope;
148
+ rationale: string;
149
+ }, opts?: {
150
+ path?: string;
151
+ now?: number;
152
+ }): boolean;
153
+ /**
154
+ * Accept an interpretation, which is the moment it starts affecting routing.
155
+ *
156
+ * Separate from `propose` on purpose: this is the review gate that keeps a researched verdict —
157
+ * a model's opinion — out of the request path until a person (or an agent acting explicitly for
158
+ * one) has looked at it.
159
+ */
160
+ export declare function acceptInterpretation(signature: string, opts?: {
161
+ path?: string;
162
+ now?: number;
163
+ override?: {
164
+ class: EligibilityClass;
165
+ scope: EligibilityScope;
166
+ };
167
+ }): boolean;
168
+ /** Drop a pending signature without accepting it — "this means nothing durable". */
169
+ export declare function rejectInterpretation(signature: string, opts?: {
170
+ path?: string;
171
+ }): boolean;
172
+ /** Every binding entry, for `llm-relay eligibility --table`. */
173
+ export declare function confirmedInterpretations(opts?: {
174
+ path?: string;
175
+ }): Array<Interpretation & {
176
+ signature: string;
177
+ }>;
178
+ /** Flush pending writes. Called on shutdown, like the other write-behind stores. */
179
+ export declare function flushInterpretations(opts?: {
180
+ path?: string;
181
+ }): void;
182
+ /** Test seam: drop the in-memory store so a suite can point at a fresh path. */
183
+ export declare function resetInterpretations(): void;
@@ -0,0 +1,330 @@
1
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir, tmpdir } from "node:os";
4
+ import { WriteBehindTimer } from "./write-behind.js";
5
+ let _store = null;
6
+ let _path = null;
7
+ const writer = new WriteBehindTimer();
8
+ /** Cap on retained unknown signatures — a misbehaving backend must not grow this without bound. */
9
+ const MAX_UNKNOWN = 200;
10
+ /** Verbatim sample cap. Enough to research from, small enough to keep the file readable. */
11
+ const SAMPLE_CHARS = 400;
12
+ function defaultPath() {
13
+ if (process.env.VITEST !== undefined) {
14
+ return join(tmpdir(), `llm-relay-test-interpretations-${process.pid}.json`);
15
+ }
16
+ const xdg = process.env.XDG_CONFIG_HOME;
17
+ const baseDir = xdg && xdg.trim() ? join(xdg, "llm-relay") : join(homedir(), ".llm-relay");
18
+ return join(baseDir, "refusal-interpretations.json");
19
+ }
20
+ /**
21
+ * Reduce a refusal message to its invariant shape.
22
+ *
23
+ * Everything that varies between two occurrences of the SAME refusal has to go, or the table gets
24
+ * one entry per request: uuids and account ids (NIM names both), numbers (credit balances, token
25
+ * counts) and urls (upgrade links carry per-request refs).
26
+ *
27
+ * ⚠ **The message text itself must survive.** An earlier version also replaced every quoted string
28
+ * with a placeholder, on the theory that quoted model names are noise. Against a live pool that
29
+ * erased the entire payload: the Anthropic front hands this the relay's own wrapper —
30
+ * `openai backend HTTP 402: {"error":"You have depleted your monthly included credits…"}` — whose
31
+ * message is *inside* quotes, so all 15 members of a real `pool/xhigh` normalized to
32
+ * `openai backend http <n>: {<name>:<name>}` and nothing could ever match. Quoted model names are
33
+ * harmless: the model is already part of the key.
34
+ *
35
+ * ⚠ For the same reason the JSON hunt digs through a WRAPPER. A body is not always JSON at the top
36
+ * level — the relay's own error envelope prefixes it with prose — so a failed parse falls back to
37
+ * the first embedded object rather than giving up.
38
+ *
39
+ * The result is kept as readable TEXT rather than a hash: a human or an agent has to read these to
40
+ * research them, and a hex digest would make the store unreviewable for no benefit.
41
+ */
42
+ export function normalizeRefusalMessage(body) {
43
+ if (typeof body !== "string")
44
+ return "";
45
+ const text = body.length > 4096 ? body.slice(0, 4096) : body;
46
+ return (extractMessage(text) ?? text)
47
+ .toLowerCase()
48
+ .replace(/https?:\/\/\S+/g, "<url>")
49
+ .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g, "<id>")
50
+ .replace(/[0-9a-z_-]{24,}/gi, "<id>")
51
+ .replace(/\d[\d,._]*/g, "<n>")
52
+ .replace(/\s+/g, " ")
53
+ .trim()
54
+ .slice(0, 240);
55
+ }
56
+ /** The human-readable message inside a body, whether it is JSON, wrapped JSON, or neither. */
57
+ function extractMessage(text) {
58
+ const candidates = [text];
59
+ // A wrapper: prose, then the real payload. Take from the first brace or bracket.
60
+ const brace = text.search(/[[{]/);
61
+ if (brace > 0)
62
+ candidates.push(text.slice(brace));
63
+ for (const candidate of candidates) {
64
+ try {
65
+ const found = findMessage(JSON.parse(candidate));
66
+ if (found)
67
+ return found;
68
+ }
69
+ catch {
70
+ // Not parseable — try the next candidate.
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+ /** Depth-first hunt for the message field an OpenAI-shaped error envelope carries. */
76
+ function findMessage(v, depth = 0) {
77
+ if (depth > 6)
78
+ return null;
79
+ if (typeof v === "string")
80
+ return v;
81
+ if (Array.isArray(v)) {
82
+ for (const x of v) {
83
+ const found = findMessage(x, depth + 1);
84
+ if (found)
85
+ return found;
86
+ }
87
+ return null;
88
+ }
89
+ if (v && typeof v === "object") {
90
+ const obj = v;
91
+ for (const field of ["message", "detail", "error", "title"]) {
92
+ if (typeof obj[field] === "string")
93
+ return obj[field];
94
+ if (obj[field] && typeof obj[field] === "object") {
95
+ const found = findMessage(obj[field], depth + 1);
96
+ if (found)
97
+ return found;
98
+ }
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+ /** The lookup key. Per (provider, model, normalized message) — see the header for why all three. */
104
+ export function refusalSignature(provider, model, status, body) {
105
+ return `${provider}|${model ?? "-"}|${status}|${normalizeRefusalMessage(body)}`;
106
+ }
107
+ /**
108
+ * Interpretations shipped with the relay, each derived from a refusal observed first-party against
109
+ * a real account (probed 2026-08-08). These bind without review: they are deterministic code in
110
+ * version control, and an operator who disagrees can override the entry.
111
+ *
112
+ * ⚠ These are matched as PATTERNS against the normalized message, not as exact signatures, because
113
+ * a seed has to cover a provider it has never been run against. That is a deliberate exception to
114
+ * the per-model keying rule above and the only one — a seed is reviewed source, whereas a
115
+ * researched verdict is a model's opinion and gets the conservative key.
116
+ *
117
+ * ⚠ **`allowance-exhausted` is not a cost verdict.** See `deployment-eligibility.ts` — it means the
118
+ * deployment is free and currently spent, and it must never evict anything from a free pool.
119
+ */
120
+ export const SEED_INTERPRETATIONS = [
121
+ {
122
+ // HuggingFace: "You have depleted your monthly included credits. Purchase pre-paid credits…"
123
+ // A BALANCE — the account's, so it covers every model behind that key.
124
+ status: (s) => s === 402,
125
+ pattern: /deplet\w*\s+your\s+(?:monthly\s+)?(?:included\s+)?credits|insufficient\s+credits|purchase\s+(?:pre-?paid\s+)?credits|out\s+of\s+credits/,
126
+ class: "allowance-exhausted",
127
+ scope: "account",
128
+ note: "stated credit balance; free but spent until it refreshes",
129
+ },
130
+ {
131
+ // Ollama Cloud: "this model requires a subscription, upgrade for access" / "requires both a
132
+ // Pro, Max, or Team plan and extra usage". About THAT MODEL under a working credential, so it
133
+ // must not block the provider's other models.
134
+ status: (s) => s === 403,
135
+ pattern: /requires?\s+(?:both\s+)?an?\s+[\w, ]*\bsubscription\b|requires?\s+(?:both\s+)?an?\s+[\w, ]*\bplan\b|upgrade\s+for\s+access/,
136
+ class: "subscription-required",
137
+ scope: "deployment",
138
+ note: "stated plan gating on one model; the credential itself is fine",
139
+ },
140
+ {
141
+ // HuggingFace: "The requested model 'x' does not exist." / OpenAI-compatible model_not_found.
142
+ // NIM: "Function '<uuid>': Not found for account '<id>'" — the serving function is gone. It
143
+ // names an account but is a fact about the deployment, not the credential.
144
+ status: (s) => s === 400 || s === 404,
145
+ pattern: /does\s+not\s+exist|model_not_found|unknown\s+model|no\s+such\s+model|not\s+found\s+for\s+account/,
146
+ class: "not-servable",
147
+ scope: "deployment",
148
+ note: "stated non-existence; catalog rot",
149
+ },
150
+ ];
151
+ function load(path) {
152
+ if (_store && _path === path)
153
+ return _store;
154
+ _path = path;
155
+ try {
156
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
157
+ if (parsed && typeof parsed === "object") {
158
+ _store = {
159
+ version: 1,
160
+ confirmed: (parsed.confirmed ?? {}),
161
+ unknown: (parsed.unknown ?? {}),
162
+ };
163
+ return _store;
164
+ }
165
+ }
166
+ catch {
167
+ // Corrupt or absent: start clean. Seeds live in source, so nothing that BINDS is lost — only
168
+ // researched entries, which the queue will re-surface as the refusals recur.
169
+ }
170
+ _store = { version: 1, confirmed: {}, unknown: {} };
171
+ return _store;
172
+ }
173
+ function persist(path) {
174
+ if (!_store)
175
+ return;
176
+ try {
177
+ mkdirSync(join(path, ".."), { recursive: true });
178
+ const tmp = `${path}.${process.pid}.tmp`;
179
+ writeFileSync(tmp, JSON.stringify(_store, null, 2) + "\n", "utf8");
180
+ renameSync(tmp, path);
181
+ }
182
+ catch {
183
+ /* storage problem, never a request failure — same contract as every other store here */
184
+ }
185
+ }
186
+ /**
187
+ * What this refusal means, or null when nothing confirmed covers it.
188
+ *
189
+ * Order: the operator-visible table first (so an override beats a seed), then the seeds. A miss
190
+ * returns null and the caller must learn nothing — that is the whole fail-safe.
191
+ */
192
+ export function interpretRefusal(provider, model, status, body, opts = {}) {
193
+ const store = load(opts.path ?? defaultPath());
194
+ const hit = store.confirmed[refusalSignature(provider, model, status, body)];
195
+ // A researched entry binds only once accepted; an unaccepted one is still sitting in review.
196
+ if (hit && (hit.source === "seed" || hit.acceptedAt !== undefined))
197
+ return hit;
198
+ const normalized = normalizeRefusalMessage(body);
199
+ for (const seed of SEED_INTERPRETATIONS) {
200
+ if (!seed.status(status))
201
+ continue;
202
+ if (seed.pattern.test(normalized))
203
+ return { class: seed.class, scope: seed.scope, source: "seed" };
204
+ }
205
+ return null;
206
+ }
207
+ /**
208
+ * Hold an uninterpretable refusal for the research tier.
209
+ *
210
+ * Deliberately cheap and lossy: one verbatim sample per signature and a count. The queue exists to
211
+ * tell a researcher "this message happens, here is what it looks like", not to be a log — the
212
+ * metadata logger already covers the traffic, and it is metadata-only precisely so bodies do not
213
+ * land on disk. This file holds error bodies, which are the provider's own text about a
214
+ * deployment, never the user's prompt.
215
+ */
216
+ export function recordUnknownRefusal(provider, model, status, body, opts = {}) {
217
+ const normalized = normalizeRefusalMessage(body);
218
+ if (!normalized)
219
+ return;
220
+ const path = opts.path ?? defaultPath();
221
+ const store = load(path);
222
+ const sig = refusalSignature(provider, model, status, body);
223
+ if (store.confirmed[sig])
224
+ return;
225
+ const now = opts.now ?? Date.now();
226
+ const existing = store.unknown[sig];
227
+ if (existing) {
228
+ existing.count += 1;
229
+ existing.lastSeen = now;
230
+ }
231
+ else {
232
+ if (Object.keys(store.unknown).length >= MAX_UNKNOWN) {
233
+ // Evict the least-recently-seen: a signature nobody has hit in a long time is the one least
234
+ // worth researching, and an unbounded file would be its own defect.
235
+ let oldestKey = null;
236
+ let oldestAt = Infinity;
237
+ for (const [k, v] of Object.entries(store.unknown)) {
238
+ if (v.lastSeen < oldestAt) {
239
+ oldestAt = v.lastSeen;
240
+ oldestKey = k;
241
+ }
242
+ }
243
+ if (oldestKey)
244
+ delete store.unknown[oldestKey];
245
+ }
246
+ store.unknown[sig] = {
247
+ provider,
248
+ model: typeof model === "string" ? model : null,
249
+ status,
250
+ normalized,
251
+ sample: body.length > SAMPLE_CHARS ? body.slice(0, SAMPLE_CHARS) : body,
252
+ count: 1,
253
+ firstSeen: now,
254
+ lastSeen: now,
255
+ };
256
+ }
257
+ writer.touch(() => persist(path));
258
+ }
259
+ /** Unseen refusals, most-frequent first — the research tier's work list. */
260
+ export function pendingRefusals(opts = {}) {
261
+ const store = load(opts.path ?? defaultPath());
262
+ return Object.entries(store.unknown)
263
+ .map(([signature, v]) => ({ ...v, signature }))
264
+ .sort((a, b) => b.count - a.count || b.lastSeen - a.lastSeen);
265
+ }
266
+ /** Attach a researched verdict to a pending signature. It does NOT bind until accepted. */
267
+ export function proposeInterpretation(signature, proposal, opts = {}) {
268
+ const path = opts.path ?? defaultPath();
269
+ const store = load(path);
270
+ const pending = store.unknown[signature];
271
+ if (!pending)
272
+ return false;
273
+ pending.proposed = { ...proposal, at: opts.now ?? Date.now() };
274
+ writer.touch(() => persist(path));
275
+ return true;
276
+ }
277
+ /**
278
+ * Accept an interpretation, which is the moment it starts affecting routing.
279
+ *
280
+ * Separate from `propose` on purpose: this is the review gate that keeps a researched verdict —
281
+ * a model's opinion — out of the request path until a person (or an agent acting explicitly for
282
+ * one) has looked at it.
283
+ */
284
+ export function acceptInterpretation(signature, opts = {}) {
285
+ const path = opts.path ?? defaultPath();
286
+ const store = load(path);
287
+ const pending = store.unknown[signature];
288
+ const verdict = opts.override ?? (pending?.proposed ? { class: pending.proposed.class, scope: pending.proposed.scope } : null);
289
+ if (!verdict)
290
+ return false;
291
+ store.confirmed[signature] = {
292
+ class: verdict.class,
293
+ scope: verdict.scope,
294
+ source: "researched",
295
+ acceptedAt: opts.now ?? Date.now(),
296
+ ...(pending?.proposed?.rationale ? { rationale: pending.proposed.rationale } : {}),
297
+ };
298
+ delete store.unknown[signature];
299
+ writer.touch(() => persist(path));
300
+ return true;
301
+ }
302
+ /** Drop a pending signature without accepting it — "this means nothing durable". */
303
+ export function rejectInterpretation(signature, opts = {}) {
304
+ const path = opts.path ?? defaultPath();
305
+ const store = load(path);
306
+ if (!store.unknown[signature])
307
+ return false;
308
+ delete store.unknown[signature];
309
+ writer.touch(() => persist(path));
310
+ return true;
311
+ }
312
+ /** Every binding entry, for `llm-relay eligibility --table`. */
313
+ export function confirmedInterpretations(opts = {}) {
314
+ const store = load(opts.path ?? defaultPath());
315
+ return Object.entries(store.confirmed).map(([signature, v]) => ({ ...v, signature }));
316
+ }
317
+ /** Flush pending writes. Called on shutdown, like the other write-behind stores. */
318
+ export function flushInterpretations(opts = {}) {
319
+ if (!writer.dirty)
320
+ return;
321
+ writer.clear();
322
+ persist(opts.path ?? defaultPath());
323
+ }
324
+ /** Test seam: drop the in-memory store so a suite can point at a fresh path. */
325
+ export function resetInterpretations() {
326
+ _store = null;
327
+ _path = null;
328
+ writer.clear();
329
+ }
330
+ //# sourceMappingURL=refusal-interpretation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refusal-interpretation.js","sourceRoot":"","sources":["../src/refusal-interpretation.ts"],"names":[],"mappings":"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"}
@@ -6,6 +6,7 @@ import { buildDispatch, markExhausted, clearExhausted, OUTCOME_DEFAULT_MS } from
6
6
  import { parseHostRoutingState } from "../host-routing.js";
7
7
  import { contextWindowResolver } from "../metadata.js";
8
8
  import { snapshotContextWindow } from "../tier-data.js";
9
+ import { observedContextLimit } from "../context-limits.js";
9
10
  import { getTelemetryReport } from "../telemetry.js";
10
11
  import { baseLog } from "../request-log.js";
11
12
  const MAX_TASK_LEN = 4096;
@@ -238,7 +239,7 @@ export async function handleAdminRoutes(req, res, pathname, path, started, reqJs
238
239
  // `cachedLimits` never fetches, so a cold cache degrades to "no window stated" rather than
239
240
  // turning a dispatch query into a blocking upstream round-trip — same rule as the request
240
241
  // -path context guardrail this reads the numbers from.
241
- publishedContextWindow: contextWindowResolver((provider, model) => h.catalog.cachedLimits(provider, model)?.contextLength ?? null, snapshotContextWindow),
242
+ publishedContextWindow: contextWindowResolver((provider, model) => h.catalog.cachedLimits(provider, model)?.contextLength ?? null, snapshotContextWindow, observedContextLimit),
242
243
  });
243
244
  return ok(view, true);
244
245
  }