llm-relay 0.26.0 → 0.28.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 +16 -0
- package/dist/candidates.js +6 -0
- package/dist/candidates.js.map +1 -1
- package/dist/circuit-breaker.d.ts +16 -0
- package/dist/circuit-breaker.js +30 -0
- package/dist/circuit-breaker.js.map +1 -1
- package/dist/cli.js +33 -14
- package/dist/cli.js.map +1 -1
- package/dist/dynamic-pools.js +1 -1
- package/dist/dynamic-pools.js.map +1 -1
- package/dist/refusal-interpretation.d.ts +36 -11
- package/dist/refusal-interpretation.js +47 -3
- package/dist/refusal-interpretation.js.map +1 -1
- package/dist/server.js +23 -6
- package/dist/server.js.map +1 -1
- package/dist/target-facts.d.ts +171 -0
- package/dist/target-facts.js +246 -0
- package/dist/target-facts.js.map +1 -0
- package/package.json +1 -1
- package/skills/llm-relay/SKILL.md +15 -4
- package/dist/deployment-eligibility.d.ts +0 -134
- package/dist/deployment-eligibility.js +0 -188
- package/dist/deployment-eligibility.js.map +0 -1
|
@@ -0,0 +1,246 @@
|
|
|
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
|
+
/** Scope names in resolution order. Exported so callers cannot invent a different precedence. */
|
|
6
|
+
export const SCOPE_PRECEDENCE = ["deployment", "group", "provider", "model"];
|
|
7
|
+
/**
|
|
8
|
+
* How long each kind is believed without re-checking. All expire, because all are reversible: a
|
|
9
|
+
* de-listed model returns, a plan is upgraded, an allowance refreshes, a key is rotated.
|
|
10
|
+
*
|
|
11
|
+
* `allowance-exhausted` is deliberately the SHORTEST despite monthly credit cycles — cooling a
|
|
12
|
+
* whole provider for a month on one 402 would be catastrophic if the balance were topped up an
|
|
13
|
+
* hour later, and one re-probe an hour is cheap and self-correcting. It also matches the breaker's
|
|
14
|
+
* `QUOTA_EXHAUSTED_COOLDOWN_MS`, so the two cannot disagree about when to try again.
|
|
15
|
+
* `credential-invalid` is short for the same reason from the other direction: a rotated key must
|
|
16
|
+
* recover quickly, and any success clears it outright.
|
|
17
|
+
*/
|
|
18
|
+
export const FACT_TTL_MS = {
|
|
19
|
+
"not-servable": 6 * 60 * 60 * 1000,
|
|
20
|
+
"subscription-required": 24 * 60 * 60 * 1000,
|
|
21
|
+
"allowance-exhausted": 60 * 60 * 1000,
|
|
22
|
+
"credential-invalid": 15 * 60 * 1000,
|
|
23
|
+
// Short, and almost always superseded by a stated reset: a rate limit window is minutes, and
|
|
24
|
+
// believing a stale one keeps working capacity idle. `RATE_LIMIT_COOLDOWN_MS` in the breaker is
|
|
25
|
+
// the same figure for the same reason — the two must not disagree about when to try again.
|
|
26
|
+
"rate-limited": 2 * 60 * 1000,
|
|
27
|
+
};
|
|
28
|
+
/** Facts that make a target unfit for a FREE pool — a statement about cost or existence. */
|
|
29
|
+
const COST_BLOCKING = new Set(["not-servable", "subscription-required"]);
|
|
30
|
+
/** Facts that make a target temporarily unusable but leave its pool membership intact. */
|
|
31
|
+
const COOLING = new Set(["allowance-exhausted", "credential-invalid", "rate-limited"]);
|
|
32
|
+
let _store = null;
|
|
33
|
+
let _path = null;
|
|
34
|
+
const writer = new WriteBehindTimer();
|
|
35
|
+
function defaultPath() {
|
|
36
|
+
// ⚠ Redirected under vitest for the same reason probe-cache and context-limits are: a test's
|
|
37
|
+
// synthetic fact persisted here would evict a real deployment from the user's live pools.
|
|
38
|
+
if (process.env.VITEST !== undefined) {
|
|
39
|
+
return join(tmpdir(), `llm-relay-test-target-facts-${process.pid}.json`);
|
|
40
|
+
}
|
|
41
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
42
|
+
const baseDir = xdg && xdg.trim() ? join(xdg, "llm-relay") : join(homedir(), ".llm-relay");
|
|
43
|
+
return join(baseDir, "target-facts.json");
|
|
44
|
+
}
|
|
45
|
+
/** The storage key for a scope. Groups hash their sorted membership so two differ iff they cover
|
|
46
|
+
* different models — a group is defined by who is in it, not by a name somebody chose. */
|
|
47
|
+
function keyOf(scope) {
|
|
48
|
+
switch (scope.kind) {
|
|
49
|
+
case "deployment":
|
|
50
|
+
return `d:${scope.provider}/${scope.model}`;
|
|
51
|
+
case "group":
|
|
52
|
+
return `g:${scope.provider}/${[...scope.members].sort().join(",")}`;
|
|
53
|
+
case "provider":
|
|
54
|
+
return `p:${scope.provider}`;
|
|
55
|
+
case "model":
|
|
56
|
+
return `m:${scope.model}`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function load(path) {
|
|
60
|
+
if (_store && _path === path)
|
|
61
|
+
return _store;
|
|
62
|
+
_path = path;
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
65
|
+
if (parsed && typeof parsed === "object" && parsed.facts && typeof parsed.facts === "object") {
|
|
66
|
+
_store = { version: 1, facts: parsed.facts };
|
|
67
|
+
return _store;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Unreadable or corrupt: start clean. Everything here is re-learnable — the worst consequence
|
|
72
|
+
// is one wasted round-trip per target.
|
|
73
|
+
}
|
|
74
|
+
_store = { version: 1, facts: {} };
|
|
75
|
+
return _store;
|
|
76
|
+
}
|
|
77
|
+
function persist(path) {
|
|
78
|
+
if (!_store)
|
|
79
|
+
return;
|
|
80
|
+
try {
|
|
81
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
82
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
83
|
+
writeFileSync(tmp, JSON.stringify(_store, null, 2) + "\n", "utf8");
|
|
84
|
+
renameSync(tmp, path);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
/* storage problem, never a request failure — same contract as every other store here */
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function expiryOf(fact) {
|
|
91
|
+
return fact.until ?? fact.at + (FACT_TTL_MS[fact.kind] ?? 0);
|
|
92
|
+
}
|
|
93
|
+
/** Does this stored fact cover this deployment? */
|
|
94
|
+
function covers(fact, provider, model) {
|
|
95
|
+
switch (fact.scope.kind) {
|
|
96
|
+
case "deployment":
|
|
97
|
+
return fact.scope.provider === provider && model !== null && fact.scope.model === model;
|
|
98
|
+
case "group":
|
|
99
|
+
return fact.scope.provider === provider && model !== null && fact.scope.members.includes(model);
|
|
100
|
+
case "provider":
|
|
101
|
+
return fact.scope.provider === provider;
|
|
102
|
+
case "model":
|
|
103
|
+
return model !== null && fact.scope.model === model;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Record a fact at the scope its evidence supports.
|
|
108
|
+
*
|
|
109
|
+
* A fresh observation replaces an older one at the SAME scope: the target is the authority on
|
|
110
|
+
* itself, and one that changed its answer is telling us so. Facts at different scopes coexist —
|
|
111
|
+
* a deployment can be `subscription-required` while its provider is `allowance-exhausted`, and
|
|
112
|
+
* resolution decides which applies.
|
|
113
|
+
*/
|
|
114
|
+
export function recordFact(kind, scope, opts = {}) {
|
|
115
|
+
const path = opts.path ?? defaultPath();
|
|
116
|
+
const store = load(path);
|
|
117
|
+
const now = opts.now ?? Date.now();
|
|
118
|
+
const stated = typeof opts.retryAfterMs === "number" && opts.retryAfterMs > 0 ? opts.retryAfterMs : null;
|
|
119
|
+
store.facts[keyOf(scope)] = {
|
|
120
|
+
kind,
|
|
121
|
+
scope,
|
|
122
|
+
at: now,
|
|
123
|
+
// A vendor-stated reset beats our TTL, exactly as it does for the breaker's cooldown: the
|
|
124
|
+
// provider knows when its own allowance refreshes and we are guessing.
|
|
125
|
+
...(stated !== null ? { until: now + stated } : {}),
|
|
126
|
+
};
|
|
127
|
+
writer.touch(() => persist(path));
|
|
128
|
+
}
|
|
129
|
+
/** Every live fact covering a deployment, most specific first. */
|
|
130
|
+
export function factsFor(provider, model, opts = {}) {
|
|
131
|
+
const store = load(opts.path ?? defaultPath());
|
|
132
|
+
const now = opts.now ?? Date.now();
|
|
133
|
+
const m = typeof model === "string" ? model : null;
|
|
134
|
+
const hits = [];
|
|
135
|
+
for (const fact of Object.values(store.facts)) {
|
|
136
|
+
const until = expiryOf(fact);
|
|
137
|
+
if (now >= until)
|
|
138
|
+
continue;
|
|
139
|
+
if (!covers(fact, provider, m))
|
|
140
|
+
continue;
|
|
141
|
+
hits.push({ kind: fact.kind, scope: fact.scope, until });
|
|
142
|
+
}
|
|
143
|
+
hits.sort((a, b) => SCOPE_PRECEDENCE.indexOf(a.scope.kind) - SCOPE_PRECEDENCE.indexOf(b.scope.kind));
|
|
144
|
+
return hits;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Has this deployment been PROVEN unfit for a free pool?
|
|
148
|
+
*
|
|
149
|
+
* ⚠ True only for facts in `COST_BLOCKING`. `allowance-exhausted` is deliberately excluded and
|
|
150
|
+
* must stay excluded — see the `FactKind` note. `credential-invalid` is excluded too: a bad key is
|
|
151
|
+
* a configuration problem, not a statement that the deployment costs money, and removing members
|
|
152
|
+
* from a pool over it would empty the pool on a mistake that a rotation fixes in seconds.
|
|
153
|
+
*/
|
|
154
|
+
export function isCostBlocked(provider, model, opts = {}) {
|
|
155
|
+
return factsFor(provider, model, opts).some((f) => COST_BLOCKING.has(f.kind));
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* When a temporarily-unusable target is worth trying again, or null when nothing is cooling it.
|
|
159
|
+
* Only `COOLING` kinds produce this — the others are exclusions, not cooldowns, and a caller that
|
|
160
|
+
* wants those wants `isCostBlocked`.
|
|
161
|
+
*/
|
|
162
|
+
export function cooldownUntil(provider, model, opts = {}) {
|
|
163
|
+
let soonest = null;
|
|
164
|
+
for (const f of factsFor(provider, model, opts)) {
|
|
165
|
+
if (!COOLING.has(f.kind))
|
|
166
|
+
continue;
|
|
167
|
+
// The LATEST expiry among covering facts: a target under both an account exhaustion and its
|
|
168
|
+
// own is usable only once both have cleared.
|
|
169
|
+
soonest = soonest === null ? f.until : Math.max(soonest, f.until);
|
|
170
|
+
}
|
|
171
|
+
return soonest;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* A success clears every fact covering this deployment — including its PROVIDER-scoped ones.
|
|
175
|
+
*
|
|
176
|
+
* A served request is first-party proof that the deployment exists, the credential authenticates,
|
|
177
|
+
* and the allowance is not spent. That is strictly better evidence than any stored refusal, and it
|
|
178
|
+
* covers the cases that matter most: credits topped up, a key rotated, a month rolled over, all
|
|
179
|
+
* well before the TTL would have expired. Same contract as the breaker clearing a credential fault
|
|
180
|
+
* on success.
|
|
181
|
+
*
|
|
182
|
+
* ⚠ A group fact is cleared only if this model is IN the group — one member serving says nothing
|
|
183
|
+
* about the others, and dropping the whole verdict would re-admit models that are genuinely gated.
|
|
184
|
+
*/
|
|
185
|
+
export function clearFacts(provider, model, opts = {}) {
|
|
186
|
+
const path = opts.path ?? defaultPath();
|
|
187
|
+
const store = load(path);
|
|
188
|
+
const m = typeof model === "string" ? model : null;
|
|
189
|
+
const cleared = [];
|
|
190
|
+
let changed = false;
|
|
191
|
+
for (const [key, fact] of Object.entries(store.facts)) {
|
|
192
|
+
if (!covers(fact, provider, m))
|
|
193
|
+
continue;
|
|
194
|
+
changed = true;
|
|
195
|
+
// Reported back so the caller can clear the SYMPTOMS of a fact that has just been disproved.
|
|
196
|
+
// A stated bad credential leaves a per-deployment 401 on the breaker for every model that
|
|
197
|
+
// tried during the outage; when the key starts working, those are stale evidence about a
|
|
198
|
+
// problem that no longer exists, and expiring them one by one keeps the pool narrow.
|
|
199
|
+
if (fact.scope.kind === "provider")
|
|
200
|
+
cleared.push(fact.kind);
|
|
201
|
+
delete store.facts[key];
|
|
202
|
+
}
|
|
203
|
+
if (changed)
|
|
204
|
+
writer.touch(() => persist(path));
|
|
205
|
+
return cleared;
|
|
206
|
+
}
|
|
207
|
+
/** Every live fact, for `llm-relay eligibility`. Expired entries are omitted, not reported. */
|
|
208
|
+
export function allFacts(opts = {}) {
|
|
209
|
+
const store = load(opts.path ?? defaultPath());
|
|
210
|
+
const now = opts.now ?? Date.now();
|
|
211
|
+
const out = [];
|
|
212
|
+
for (const fact of Object.values(store.facts)) {
|
|
213
|
+
const until = expiryOf(fact);
|
|
214
|
+
if (now >= until)
|
|
215
|
+
continue;
|
|
216
|
+
out.push({ kind: fact.kind, scope: fact.scope, at: fact.at, until });
|
|
217
|
+
}
|
|
218
|
+
return out.sort((a, b) => b.at - a.at);
|
|
219
|
+
}
|
|
220
|
+
/** A human-readable label for a scope — used by the CLI and by nothing that makes decisions. */
|
|
221
|
+
export function describeScope(scope) {
|
|
222
|
+
switch (scope.kind) {
|
|
223
|
+
case "deployment":
|
|
224
|
+
return `${scope.provider}/${scope.model}`;
|
|
225
|
+
case "group":
|
|
226
|
+
return `${scope.provider}/{${scope.members.length} models}`;
|
|
227
|
+
case "provider":
|
|
228
|
+
return `${scope.provider}/* (whole account)`;
|
|
229
|
+
case "model":
|
|
230
|
+
return `*/${scope.model}`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/** Flush pending writes. Called on shutdown, like the other write-behind stores. */
|
|
234
|
+
export function flushFacts(opts = {}) {
|
|
235
|
+
if (!writer.dirty)
|
|
236
|
+
return;
|
|
237
|
+
writer.clear();
|
|
238
|
+
persist(opts.path ?? defaultPath());
|
|
239
|
+
}
|
|
240
|
+
/** Test seam: drop the in-memory store so a suite can point at a fresh path. */
|
|
241
|
+
export function resetFacts() {
|
|
242
|
+
_store = null;
|
|
243
|
+
_path = null;
|
|
244
|
+
writer.clear();
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=target-facts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"target-facts.js","sourceRoot":"","sources":["../src/target-facts.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -495,13 +495,24 @@ constrained to the three classes and two scopes, a signature is keyed per (provi
|
|
|
495
495
|
provider's message can never produce a verdict about another, and the user accepts. Do not weaken
|
|
496
496
|
any of them to "save a step".
|
|
497
497
|
|
|
498
|
-
Choosing
|
|
498
|
+
Choosing the class, and the scope it applies to:
|
|
499
499
|
|
|
500
500
|
| The message states | Class | Scope |
|
|
501
501
|
|---|---|---|
|
|
502
|
-
| the model does not exist / is not found for the account | `not-servable` | deployment |
|
|
503
|
-
| a plan or subscription is needed for **this model** | `subscription-required` | deployment |
|
|
504
|
-
|
|
|
502
|
+
| the model does not exist / is not found for the account | `not-servable` | `deployment` |
|
|
503
|
+
| a plan or subscription is needed for **this model** | `subscription-required` | `deployment` |
|
|
504
|
+
| a plan is needed for a **named family** of models | `subscription-required` | `group` + `--members` |
|
|
505
|
+
| the **account's** credits or allowance are spent | `allowance-exhausted` | `provider` |
|
|
506
|
+
| the **key itself** is invalid / revoked | `credential-invalid` | `provider` |
|
|
507
|
+
|
|
508
|
+
Scope is the half most worth getting right: `provider` means one observation covers every model
|
|
509
|
+
behind that credential, which is exactly what stops a pool spending one round-trip per member to
|
|
510
|
+
rediscover one balance. But it is also what takes out a whole provider if you are wrong, so
|
|
511
|
+
**scope by what the message states, never by what you infer from a pattern of failures.** Several
|
|
512
|
+
models failing identically is equally several gated models under a working key.
|
|
513
|
+
|
|
514
|
+
⚠ **A `group` verdict must name its members** (`--members id1,id2`) — there is no family registry
|
|
515
|
+
and no prefix matching, deliberately. If you cannot enumerate the family, use `deployment`.
|
|
505
516
|
|
|
506
517
|
⚠ The third is not a cost verdict. A free lane that has spent this period's allowance is still
|
|
507
518
|
free, and marking it otherwise would evict it from every free pool long after the credits refresh.
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deployment eligibility LEARNED from what a backend said when it refused to serve.
|
|
3
|
-
*
|
|
4
|
-
* Sibling of `context-limits.ts`, and built on the same rule: only an EXPLICITLY STATED fact is
|
|
5
|
-
* recorded. A bare 403 teaches nothing (it could be a revoked key, which is the credential axis's
|
|
6
|
-
* job); a 403 that says "this model requires a subscription" is the provider stating that this
|
|
7
|
-
* deployment is not included in the plan we hold.
|
|
8
|
-
*
|
|
9
|
-
* This module is the CONSEQUENCE half only. Deciding what a given refusal message means belongs to
|
|
10
|
-
* `refusal-interpretation.ts`, which answers it by deterministic lookup and queues anything it
|
|
11
|
-
* cannot answer for offline research. Nothing here interprets anything; it stores verdicts and
|
|
12
|
-
* reports what they imply for admission and ordering.
|
|
13
|
-
*
|
|
14
|
-
* Why this exists at all: `assessCost()` admits a model from a `tierType: "free"` provider with
|
|
15
|
-
* unpublished prices as free on the `provider-tier` basis. That is the right default — most free
|
|
16
|
-
* providers publish no prices — but it is an assumption about a ROSTER, and a roster contains
|
|
17
|
-
* subscription-gated SKUs and models that have been de-listed behind the scenes. Those are only
|
|
18
|
-
* ever discovered by asking. This store is where the answer goes so the next request does not have
|
|
19
|
-
* to ask again.
|
|
20
|
-
*
|
|
21
|
-
* ⚠ **The three classes are NOT interchangeable, and the distinction is the point.**
|
|
22
|
-
*
|
|
23
|
-
* not-servable — the deployment does not exist (404/400 "does not exist", "not found
|
|
24
|
-
* for account"). An EXISTENCE fact. Drop it from pool admission.
|
|
25
|
-
* subscription-required — it exists, but is not covered by the plan we hold (403 "requires a
|
|
26
|
-
* subscription"). A COST fact: it refutes the `provider-tier` free
|
|
27
|
-
* assessment for this one deployment. Drop it from FREE pool admission.
|
|
28
|
-
* allowance-exhausted — it exists AND it is free; the free allowance is spent until it
|
|
29
|
-
* refreshes (402 "you have depleted your monthly included credits").
|
|
30
|
-
* A TEMPORAL fact, and emphatically NOT a cost fact.
|
|
31
|
-
*
|
|
32
|
-
* **Never let `allowance-exhausted` mean "paid".** A free-tier account that has spent this
|
|
33
|
-
* period's credits is the single most common state of every provider this proxy fronts — it is the
|
|
34
|
-
* normal condition of a working free lane, not a discovery about its price. Reclassifying it as
|
|
35
|
-
* paid would evict the deployment from every free pool on a condition that clears by itself, and
|
|
36
|
-
* the eviction would outlive the exhaustion. So it is deliberately unreachable from the cost path:
|
|
37
|
-
* `isCostBlocked()` does not consider it, and only `cooldownUntil()` reports it — a demotion, on
|
|
38
|
-
* the same footing as a 429, that expires on its own and clears the moment anything succeeds.
|
|
39
|
-
*/
|
|
40
|
-
/** What a refusal proved about a deployment. See the header — these are not ranks of one scale. */
|
|
41
|
-
export type EligibilityClass = "not-servable" | "subscription-required" | "allowance-exhausted";
|
|
42
|
-
/**
|
|
43
|
-
* Whose fact it is.
|
|
44
|
-
*
|
|
45
|
-
* `deployment` — about this (provider, model) alone.
|
|
46
|
-
* `account` — about the credential, so every deployment on that provider shares it. Reserved
|
|
47
|
-
* for refusals that state an ACCOUNT-level condition ("you have depleted your
|
|
48
|
-
* monthly included credits" names a balance, not a model). This is what stops a
|
|
49
|
-
* pool holding six HuggingFace members from spending six round-trips to rediscover
|
|
50
|
-
* one balance — measured on this machine, `pool/xhigh`'s 15 members resolve to only
|
|
51
|
-
* four independent quota domains.
|
|
52
|
-
*/
|
|
53
|
-
export type EligibilityScope = "deployment" | "account";
|
|
54
|
-
/**
|
|
55
|
-
* How long each class is believed without re-checking.
|
|
56
|
-
*
|
|
57
|
-
* All three expire, because all three are reversible: a de-listed model can come back, a plan can
|
|
58
|
-
* be upgraded, and an allowance always refreshes. The spread reflects how fast each moves, and
|
|
59
|
-
* `allowance-exhausted` is deliberately the SHORTEST — HuggingFace's included credits are monthly,
|
|
60
|
-
* but cooling a whole provider for a month on one 402 would be catastrophic if the balance were
|
|
61
|
-
* topped up an hour later. One re-probe an hour costs one request and is self-correcting; it also
|
|
62
|
-
* matches the breaker's own `QUOTA_EXHAUSTED_COOLDOWN_MS`, so the two cannot disagree about when a
|
|
63
|
-
* quota-exhausted target is worth trying again.
|
|
64
|
-
*/
|
|
65
|
-
export declare const ELIGIBILITY_TTL_MS: Record<EligibilityClass, number>;
|
|
66
|
-
/**
|
|
67
|
-
* Record what a deployment stated about itself. A fresh observation always replaces an older one:
|
|
68
|
-
* the deployment is the authority, and one that changed its answer is telling us so.
|
|
69
|
-
*/
|
|
70
|
-
export declare function recordEligibility(provider: string, model: string, observation: {
|
|
71
|
-
class: EligibilityClass;
|
|
72
|
-
scope: EligibilityScope;
|
|
73
|
-
}, opts?: {
|
|
74
|
-
path?: string;
|
|
75
|
-
now?: number;
|
|
76
|
-
retryAfterMs?: number | null;
|
|
77
|
-
}): void;
|
|
78
|
-
/** The live observation covering a deployment (its own, else its provider's), or null. */
|
|
79
|
-
export declare function observedEligibility(provider: string, model: string | null | undefined, opts?: {
|
|
80
|
-
path?: string;
|
|
81
|
-
now?: number;
|
|
82
|
-
}): {
|
|
83
|
-
class: EligibilityClass;
|
|
84
|
-
scope: EligibilityScope;
|
|
85
|
-
} | null;
|
|
86
|
-
/**
|
|
87
|
-
* Has this deployment been PROVEN unfit for a free pool?
|
|
88
|
-
*
|
|
89
|
-
* ⚠ True only for `not-servable` and `subscription-required`. `allowance-exhausted` is
|
|
90
|
-
* deliberately absent and must stay absent — see the file header. A spent allowance says nothing
|
|
91
|
-
* about what the deployment costs, and evicting it from the pool would outlive the exhaustion that
|
|
92
|
-
* caused it.
|
|
93
|
-
*/
|
|
94
|
-
export declare function isCostBlocked(provider: string, model: string | null | undefined, opts?: {
|
|
95
|
-
path?: string;
|
|
96
|
-
now?: number;
|
|
97
|
-
}): boolean;
|
|
98
|
-
/**
|
|
99
|
-
* When a temporarily-unavailable deployment is worth trying again, or null when nothing is cooling
|
|
100
|
-
* it. Only `allowance-exhausted` produces a cooldown — the other two classes are not cooling, they
|
|
101
|
-
* are excluded, and a caller that wants those wants `isCostBlocked`.
|
|
102
|
-
*/
|
|
103
|
-
export declare function cooldownUntil(provider: string, model: string | null | undefined, opts?: {
|
|
104
|
-
path?: string;
|
|
105
|
-
now?: number;
|
|
106
|
-
}): number | null;
|
|
107
|
-
/**
|
|
108
|
-
* A success clears what the deployment previously refused — including its ACCOUNT record.
|
|
109
|
-
*
|
|
110
|
-
* A served request is first-party proof that the credential has allowance right now, which is a
|
|
111
|
-
* strictly better signal than an hour-old 402 and covers the case that matters most: credits
|
|
112
|
-
* topped up, or the month rolled over, well before the TTL would have expired. Same contract as
|
|
113
|
-
* the breaker clearing a credential fault on success.
|
|
114
|
-
*/
|
|
115
|
-
export declare function clearEligibility(provider: string, model: string | null | undefined, opts?: {
|
|
116
|
-
path?: string;
|
|
117
|
-
}): void;
|
|
118
|
-
/** Every live observation, for `llm-relay eligibility`. Expired entries are omitted, not reported. */
|
|
119
|
-
export declare function allObservations(opts?: {
|
|
120
|
-
path?: string;
|
|
121
|
-
now?: number;
|
|
122
|
-
}): Array<{
|
|
123
|
-
key: string;
|
|
124
|
-
class: EligibilityClass;
|
|
125
|
-
scope: EligibilityScope;
|
|
126
|
-
at: number;
|
|
127
|
-
until: number;
|
|
128
|
-
}>;
|
|
129
|
-
/** Flush pending observations. Called on shutdown, like the other write-behind stores. */
|
|
130
|
-
export declare function flushEligibility(opts?: {
|
|
131
|
-
path?: string;
|
|
132
|
-
}): void;
|
|
133
|
-
/** Test seam: drop the in-memory store so a suite can point at a fresh path. */
|
|
134
|
-
export declare function resetEligibility(): void;
|
|
@@ -1,188 +0,0 @@
|
|
|
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
|
-
/**
|
|
6
|
-
* How long each class is believed without re-checking.
|
|
7
|
-
*
|
|
8
|
-
* All three expire, because all three are reversible: a de-listed model can come back, a plan can
|
|
9
|
-
* be upgraded, and an allowance always refreshes. The spread reflects how fast each moves, and
|
|
10
|
-
* `allowance-exhausted` is deliberately the SHORTEST — HuggingFace's included credits are monthly,
|
|
11
|
-
* but cooling a whole provider for a month on one 402 would be catastrophic if the balance were
|
|
12
|
-
* topped up an hour later. One re-probe an hour costs one request and is self-correcting; it also
|
|
13
|
-
* matches the breaker's own `QUOTA_EXHAUSTED_COOLDOWN_MS`, so the two cannot disagree about when a
|
|
14
|
-
* quota-exhausted target is worth trying again.
|
|
15
|
-
*/
|
|
16
|
-
export const ELIGIBILITY_TTL_MS = {
|
|
17
|
-
"not-servable": 6 * 60 * 60 * 1000,
|
|
18
|
-
"subscription-required": 24 * 60 * 60 * 1000,
|
|
19
|
-
"allowance-exhausted": 60 * 60 * 1000,
|
|
20
|
-
};
|
|
21
|
-
let _store = null;
|
|
22
|
-
let _path = null;
|
|
23
|
-
const writer = new WriteBehindTimer();
|
|
24
|
-
function defaultPath() {
|
|
25
|
-
// ⚠ Redirected under vitest for the same reason probe-cache and context-limits are: a test's
|
|
26
|
-
// synthetic refusal persisted here would evict a real deployment from the user's live pools.
|
|
27
|
-
if (process.env.VITEST !== undefined) {
|
|
28
|
-
return join(tmpdir(), `llm-relay-test-eligibility-${process.pid}.json`);
|
|
29
|
-
}
|
|
30
|
-
const xdg = process.env.XDG_CONFIG_HOME;
|
|
31
|
-
const baseDir = xdg && xdg.trim() ? join(xdg, "llm-relay") : join(homedir(), ".llm-relay");
|
|
32
|
-
return join(baseDir, "deployment-eligibility.json");
|
|
33
|
-
}
|
|
34
|
-
function deploymentKey(provider, model) {
|
|
35
|
-
return `${provider}/${model}`;
|
|
36
|
-
}
|
|
37
|
-
function accountKey(provider) {
|
|
38
|
-
return `${provider}/*`;
|
|
39
|
-
}
|
|
40
|
-
function load(path) {
|
|
41
|
-
if (_store && _path === path)
|
|
42
|
-
return _store;
|
|
43
|
-
_path = path;
|
|
44
|
-
try {
|
|
45
|
-
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
46
|
-
if (parsed && typeof parsed === "object" && parsed.observations && typeof parsed.observations === "object") {
|
|
47
|
-
_store = { version: 1, observations: parsed.observations };
|
|
48
|
-
return _store;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
// Unreadable or corrupt: start clean. Everything here is a re-learnable optimization — the
|
|
53
|
-
// worst consequence of losing the file is one wasted round-trip per deployment.
|
|
54
|
-
}
|
|
55
|
-
_store = { version: 1, observations: {} };
|
|
56
|
-
return _store;
|
|
57
|
-
}
|
|
58
|
-
function persist(path) {
|
|
59
|
-
if (!_store)
|
|
60
|
-
return;
|
|
61
|
-
try {
|
|
62
|
-
mkdirSync(join(path, ".."), { recursive: true });
|
|
63
|
-
const tmp = `${path}.${process.pid}.tmp`;
|
|
64
|
-
writeFileSync(tmp, JSON.stringify(_store, null, 2) + "\n", "utf8");
|
|
65
|
-
renameSync(tmp, path);
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
// Same contract as every other store here: a full disk is a storage problem, never a request
|
|
69
|
-
// failure. The observation stays in memory for this process's lifetime.
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Record what a deployment stated about itself. A fresh observation always replaces an older one:
|
|
74
|
-
* the deployment is the authority, and one that changed its answer is telling us so.
|
|
75
|
-
*/
|
|
76
|
-
export function recordEligibility(provider, model, observation, opts = {}) {
|
|
77
|
-
const path = opts.path ?? defaultPath();
|
|
78
|
-
const store = load(path);
|
|
79
|
-
const now = opts.now ?? Date.now();
|
|
80
|
-
const key = observation.scope === "account" ? accountKey(provider) : deploymentKey(provider, model);
|
|
81
|
-
const stated = typeof opts.retryAfterMs === "number" && opts.retryAfterMs > 0 ? opts.retryAfterMs : null;
|
|
82
|
-
store.observations[key] = {
|
|
83
|
-
class: observation.class,
|
|
84
|
-
scope: observation.scope,
|
|
85
|
-
at: now,
|
|
86
|
-
// A vendor-stated reset beats our TTL, exactly as it does for the breaker's cooldown: the
|
|
87
|
-
// provider knows when its own allowance refreshes and we are guessing.
|
|
88
|
-
...(stated !== null ? { until: now + stated } : {}),
|
|
89
|
-
};
|
|
90
|
-
writer.touch(() => persist(path));
|
|
91
|
-
}
|
|
92
|
-
/** The live observation covering a deployment (its own, else its provider's), or null. */
|
|
93
|
-
export function observedEligibility(provider, model, opts = {}) {
|
|
94
|
-
const path = opts.path ?? defaultPath();
|
|
95
|
-
const store = load(path);
|
|
96
|
-
const now = opts.now ?? Date.now();
|
|
97
|
-
const keys = typeof model === "string" ? [deploymentKey(provider, model), accountKey(provider)] : [accountKey(provider)];
|
|
98
|
-
for (const key of keys) {
|
|
99
|
-
const hit = store.observations[key];
|
|
100
|
-
if (!hit)
|
|
101
|
-
continue;
|
|
102
|
-
const expiry = hit.until ?? hit.at + (ELIGIBILITY_TTL_MS[hit.class] ?? 0);
|
|
103
|
-
if (now >= expiry)
|
|
104
|
-
continue;
|
|
105
|
-
return { class: hit.class, scope: hit.scope };
|
|
106
|
-
}
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Has this deployment been PROVEN unfit for a free pool?
|
|
111
|
-
*
|
|
112
|
-
* ⚠ True only for `not-servable` and `subscription-required`. `allowance-exhausted` is
|
|
113
|
-
* deliberately absent and must stay absent — see the file header. A spent allowance says nothing
|
|
114
|
-
* about what the deployment costs, and evicting it from the pool would outlive the exhaustion that
|
|
115
|
-
* caused it.
|
|
116
|
-
*/
|
|
117
|
-
export function isCostBlocked(provider, model, opts = {}) {
|
|
118
|
-
const hit = observedEligibility(provider, model, opts);
|
|
119
|
-
return hit !== null && hit.class !== "allowance-exhausted";
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* When a temporarily-unavailable deployment is worth trying again, or null when nothing is cooling
|
|
123
|
-
* it. Only `allowance-exhausted` produces a cooldown — the other two classes are not cooling, they
|
|
124
|
-
* are excluded, and a caller that wants those wants `isCostBlocked`.
|
|
125
|
-
*/
|
|
126
|
-
export function cooldownUntil(provider, model, opts = {}) {
|
|
127
|
-
const path = opts.path ?? defaultPath();
|
|
128
|
-
const store = load(path);
|
|
129
|
-
const now = opts.now ?? Date.now();
|
|
130
|
-
const keys = typeof model === "string" ? [deploymentKey(provider, model), accountKey(provider)] : [accountKey(provider)];
|
|
131
|
-
for (const key of keys) {
|
|
132
|
-
const hit = store.observations[key];
|
|
133
|
-
if (!hit || hit.class !== "allowance-exhausted")
|
|
134
|
-
continue;
|
|
135
|
-
const expiry = hit.until ?? hit.at + ELIGIBILITY_TTL_MS[hit.class];
|
|
136
|
-
if (now < expiry)
|
|
137
|
-
return expiry;
|
|
138
|
-
}
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* A success clears what the deployment previously refused — including its ACCOUNT record.
|
|
143
|
-
*
|
|
144
|
-
* A served request is first-party proof that the credential has allowance right now, which is a
|
|
145
|
-
* strictly better signal than an hour-old 402 and covers the case that matters most: credits
|
|
146
|
-
* topped up, or the month rolled over, well before the TTL would have expired. Same contract as
|
|
147
|
-
* the breaker clearing a credential fault on success.
|
|
148
|
-
*/
|
|
149
|
-
export function clearEligibility(provider, model, opts = {}) {
|
|
150
|
-
const path = opts.path ?? defaultPath();
|
|
151
|
-
const store = load(path);
|
|
152
|
-
let changed = false;
|
|
153
|
-
for (const key of typeof model === "string" ? [deploymentKey(provider, model), accountKey(provider)] : [accountKey(provider)]) {
|
|
154
|
-
if (store.observations[key]) {
|
|
155
|
-
delete store.observations[key];
|
|
156
|
-
changed = true;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
if (changed)
|
|
160
|
-
writer.touch(() => persist(path));
|
|
161
|
-
}
|
|
162
|
-
/** Every live observation, for `llm-relay eligibility`. Expired entries are omitted, not reported. */
|
|
163
|
-
export function allObservations(opts = {}) {
|
|
164
|
-
const store = load(opts.path ?? defaultPath());
|
|
165
|
-
const now = opts.now ?? Date.now();
|
|
166
|
-
const out = [];
|
|
167
|
-
for (const [key, hit] of Object.entries(store.observations)) {
|
|
168
|
-
const until = hit.until ?? hit.at + (ELIGIBILITY_TTL_MS[hit.class] ?? 0);
|
|
169
|
-
if (now >= until)
|
|
170
|
-
continue;
|
|
171
|
-
out.push({ key, class: hit.class, scope: hit.scope, at: hit.at, until });
|
|
172
|
-
}
|
|
173
|
-
return out.sort((a, b) => b.at - a.at);
|
|
174
|
-
}
|
|
175
|
-
/** Flush pending observations. Called on shutdown, like the other write-behind stores. */
|
|
176
|
-
export function flushEligibility(opts = {}) {
|
|
177
|
-
if (!writer.dirty)
|
|
178
|
-
return;
|
|
179
|
-
writer.clear();
|
|
180
|
-
persist(opts.path ?? defaultPath());
|
|
181
|
-
}
|
|
182
|
-
/** Test seam: drop the in-memory store so a suite can point at a fresh path. */
|
|
183
|
-
export function resetEligibility() {
|
|
184
|
-
_store = null;
|
|
185
|
-
_path = null;
|
|
186
|
-
writer.clear();
|
|
187
|
-
}
|
|
188
|
-
//# sourceMappingURL=deployment-eligibility.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"deployment-eligibility.js","sourceRoot":"","sources":["../src/deployment-eligibility.ts"],"names":[],"mappings":"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"}
|