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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-relay",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "description": "Loopback bidirectional Anthropic/OpenAI API proxy with tool-call validation and multi-provider routing.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -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 between the three classes:
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
- | the **account's** credits or allowance are spent | `allowance-exhausted` | account |
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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"}