llm-relay 0.55.2 → 0.56.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/cli.js +58 -17
- package/dist/cli.js.map +1 -1
- package/dist/ping/cadence.d.ts +21 -0
- package/dist/ping/cadence.js +49 -0
- package/dist/ping/cadence.js.map +1 -1
- package/dist/refusal-interpretation.d.ts +12 -0
- package/dist/refusal-interpretation.js +15 -1
- package/dist/refusal-interpretation.js.map +1 -1
- package/dist/spend-headroom.d.ts +65 -0
- package/dist/spend-headroom.js +48 -0
- package/dist/spend-headroom.js.map +1 -0
- package/dist/target-facts.d.ts +14 -0
- package/dist/target-facts.js +19 -4
- package/dist/target-facts.js.map +1 -1
- package/package.json +1 -1
- package/skills/llm-relay/SKILL.md +5 -3
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import { contextWindowResolver, COST_CLASSES } from "./metadata.js";
|
|
|
20
20
|
import { snapshotContextWindow } from "./tier-data.js";
|
|
21
21
|
import { observedContextLimit, flushObservedContextLimits } from "./context-limits.js";
|
|
22
22
|
import { allFacts, describeScope, flushFacts, FACT_KINDS } from "./target-facts.js";
|
|
23
|
-
import { acceptInterpretation, flushInterpretations, pendingRefusals, proposeInterpretation, rejectInterpretation, } from "./refusal-interpretation.js";
|
|
23
|
+
import { acceptInterpretation, flushInterpretations, pendingRefusals, proposeInterpretation, rejectInterpretation, signatureDigest, } from "./refusal-interpretation.js";
|
|
24
24
|
import { installAgentHook, removeAgentHook, agentHookInstalled } from "./claude-hook.js";
|
|
25
25
|
import { installProcessSafetyNet } from "./process-safety-net.js";
|
|
26
26
|
import { createProxy } from "./server.js";
|
|
@@ -76,6 +76,7 @@ const VALUE_FLAGS = new Set([
|
|
|
76
76
|
"--rationale", "-rationale",
|
|
77
77
|
"--reset-field", "-reset-field",
|
|
78
78
|
"--reset-ms", "-reset-ms",
|
|
79
|
+
"--sig", "-sig",
|
|
79
80
|
"--import", "-import",
|
|
80
81
|
"--label", "-label",
|
|
81
82
|
"--env-name", "-env-name",
|
|
@@ -194,7 +195,7 @@ ${formatTextTable([
|
|
|
194
195
|
["llm-relay candidates [-p <name>]", "Compare deployment x credential-slot targets."],
|
|
195
196
|
["llm-relay cost [--window <w>] [--by <d>] [--include-repair]", "Summarise spend from the local accounting ledger."],
|
|
196
197
|
["llm-relay eligibility", "What backends said about themselves; refusals awaiting interpretation."],
|
|
197
|
-
["llm-relay eligibility <propose|accept> <n> --class <kind> --scope <scope>", "Scopes: attempt | group | deployment | credential | provider | model."],
|
|
198
|
+
["llm-relay eligibility <propose|accept> <n> [--sig <digest>] --class <kind> --scope <scope>", "Scopes: attempt | group | deployment | credential | provider | model. --sig pins the item across queue reorderings."],
|
|
198
199
|
["llm-relay eligibility ... --scope group --members <id,id,...> [--all-credentials]", "Groups default to the current credential slot; the flag explicitly widens them."],
|
|
199
200
|
["llm-relay eligibility ... --cost-class free|paid|unknown", "Cover only deployments of that cost class, resolved live from catalog prices."],
|
|
200
201
|
["eligibility scope breadth", "credential = current credential slot; provider = all credentials for that provider."],
|
|
@@ -2242,25 +2243,58 @@ const FACT_MEANING = {
|
|
|
2242
2243
|
* proposal whose accept command drops the flag commits the credential-wide verdict that the flag
|
|
2243
2244
|
* exists to prevent. Add the flag here in the same change that adds it above.
|
|
2244
2245
|
*/
|
|
2245
|
-
function eligibilityAcceptCommand(index, cls, scope, reset, costClasses) {
|
|
2246
|
+
function eligibilityAcceptCommand(index, signature, cls, scope, reset, costClasses) {
|
|
2246
2247
|
const cost = costClasses === undefined || costClasses.length === 0
|
|
2247
2248
|
? ""
|
|
2248
2249
|
: ` --cost-class ${costClasses.join(",")}`;
|
|
2249
|
-
|
|
2250
|
+
// `--sig` keys the accept to the SIGNATURE, so a queue that reorders between propose and accept
|
|
2251
|
+
// cannot land the verdict on a different refusal — the index alone did, twice.
|
|
2252
|
+
return `llm-relay eligibility accept ${index} --sig ${signatureDigest(signature)} --class ${cls} --scope ${eligibilityScopeArgs(scope)}${eligibilityResetArgs(reset)}${cost}`;
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Resolve which pending refusal a mutation addresses.
|
|
2256
|
+
*
|
|
2257
|
+
* Addressed by list POSITION for typing convenience — a signature is a whole normalized error
|
|
2258
|
+
* message and nobody is retyping one at a shell — but positions SHIFT between invocations (the
|
|
2259
|
+
* sort is count-then-recency), so the printed commands also carry `--sig <digest>`. When present,
|
|
2260
|
+
* the digest is authoritative: it names the signature itself and survives any reordering. The bare
|
|
2261
|
+
* index remains valid for hand-typed use against a fresh listing. Exits 1 on no resolution.
|
|
2262
|
+
*/
|
|
2263
|
+
function resolvePendingEntry(pending, action, arg) {
|
|
2264
|
+
const idx = Number(arg);
|
|
2265
|
+
const byIndex = Number.isInteger(idx) && idx >= 1 && idx <= pending.length ? pending[idx - 1] : undefined;
|
|
2266
|
+
const sigArg = argValue("--sig")?.toLowerCase();
|
|
2267
|
+
if (sigArg === undefined) {
|
|
2268
|
+
if (byIndex)
|
|
2269
|
+
return byIndex;
|
|
2270
|
+
process.stderr.write(`llm-relay eligibility: ${action} expects a pending item number 1..${pending.length}\n`);
|
|
2271
|
+
process.exit(1);
|
|
2272
|
+
return undefined;
|
|
2273
|
+
}
|
|
2274
|
+
const matches = pending.filter((p) => signatureDigest(p.signature) === sigArg);
|
|
2275
|
+
// >1 is a digest collision — not expected within a MAX_UNKNOWN-capped queue, but if it ever
|
|
2276
|
+
// happens the index disambiguates, and refusing beats guessing between two refusals.
|
|
2277
|
+
const entry = matches.length === 1 ? matches[0] : matches.find((p) => p === byIndex);
|
|
2278
|
+
if (!entry) {
|
|
2279
|
+
process.stderr.write(matches.length === 0
|
|
2280
|
+
? `llm-relay eligibility: no pending refusal matches --sig ${sigArg}; run llm-relay eligibility to list current digests\n`
|
|
2281
|
+
: `llm-relay eligibility: --sig ${sigArg} matches ${matches.length} pending refusals; pass the item number of the intended one alongside --sig\n`);
|
|
2282
|
+
process.exit(1);
|
|
2283
|
+
return undefined;
|
|
2284
|
+
}
|
|
2285
|
+
if (byIndex !== undefined && byIndex !== entry) {
|
|
2286
|
+
// The queue moved since the command was printed — exactly the case `--sig` exists for.
|
|
2287
|
+
process.stderr.write(`llm-relay eligibility: the queue reordered; --sig ${sigArg} now sits at position ${pending.indexOf(entry) + 1}, acting on it\n`);
|
|
2288
|
+
}
|
|
2289
|
+
return entry;
|
|
2250
2290
|
}
|
|
2251
2291
|
export function runEligibility(sub, arg) {
|
|
2252
2292
|
const pending = pendingRefusals();
|
|
2253
2293
|
const action = sub ?? "status";
|
|
2254
2294
|
if (action === "accept" || action === "reject" || action === "propose") {
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
const idx = Number(arg);
|
|
2258
|
-
const entry = Number.isInteger(idx) && idx >= 1 && idx <= pending.length ? pending[idx - 1] : undefined;
|
|
2259
|
-
if (!entry) {
|
|
2260
|
-
process.stderr.write(`llm-relay eligibility: ${action} expects a pending item number 1..${pending.length}\n`);
|
|
2261
|
-
process.exit(1);
|
|
2295
|
+
const entry = resolvePendingEntry(pending, action, arg);
|
|
2296
|
+
if (!entry)
|
|
2262
2297
|
return;
|
|
2263
|
-
}
|
|
2264
2298
|
if (action === "reject") {
|
|
2265
2299
|
rejectInterpretation(entry.signature);
|
|
2266
2300
|
flushInterpretations();
|
|
@@ -2354,7 +2388,7 @@ export function runEligibility(sub, arg) {
|
|
|
2354
2388
|
if (action === "propose") {
|
|
2355
2389
|
proposeInterpretation(entry.signature, { class: cls, scope, rationale, ...(reset ? { reset } : {}), ...(costFilter ? { costClasses: costFilter } : {}) });
|
|
2356
2390
|
flushInterpretations();
|
|
2357
|
-
process.stdout.write(`proposed ${cls} (${shown}) — not yet binding. Commit with: ${eligibilityAcceptCommand(
|
|
2391
|
+
process.stdout.write(`proposed ${cls} (${shown}) — not yet binding. Commit with: ${eligibilityAcceptCommand(pending.indexOf(entry) + 1, entry.signature, cls, scope, reset, costFilter)}\n`);
|
|
2358
2392
|
return;
|
|
2359
2393
|
}
|
|
2360
2394
|
acceptInterpretation(entry.signature, { override: { class: cls, scope, ...(reset ? { reset } : {}), ...(costFilter ? { costClasses: costFilter } : {}) } });
|
|
@@ -2382,7 +2416,8 @@ export function runEligibility(sub, arg) {
|
|
|
2382
2416
|
process.stdout.write(" (none; every refusal seen so far was understood)\n");
|
|
2383
2417
|
}
|
|
2384
2418
|
pending.forEach((p, i) => {
|
|
2385
|
-
|
|
2419
|
+
// The digest addresses the item across reorderings; the index is only this listing's position.
|
|
2420
|
+
process.stdout.write(`\n [${i + 1}] sig ${signatureDigest(p.signature)} ${p.provider}/${p.model ?? "-"} HTTP ${p.status} ×${p.count}\n`);
|
|
2386
2421
|
process.stdout.write(` ${p.normalized}\n`);
|
|
2387
2422
|
if (p.proposed) {
|
|
2388
2423
|
// ⚠ The scope is a STRUCTURE now, not a word — interpolating it printed "[object Object]"
|
|
@@ -2396,7 +2431,11 @@ export function runEligibility(sub, arg) {
|
|
|
2396
2431
|
if (sc.kind === "group") {
|
|
2397
2432
|
process.stdout.write(` covers: ${sc.members.join(", ")}\n`);
|
|
2398
2433
|
}
|
|
2399
|
-
|
|
2434
|
+
// ⚠ The full proposal travels — reset AND costClasses. This call once omitted `costClasses`,
|
|
2435
|
+
// so the status listing printed an accept command WIDER than the proposal it echoed: the
|
|
2436
|
+
// silent-widening hazard `eligibilityAcceptCommand`'s own comment warns about, surviving in
|
|
2437
|
+
// the one call site the v0.55.2 fix did not reach.
|
|
2438
|
+
process.stdout.write(` accept with: ${eligibilityAcceptCommand(i + 1, p.signature, p.proposed.class, sc, p.proposed.reset, p.proposed.costClasses)}\n`);
|
|
2400
2439
|
}
|
|
2401
2440
|
});
|
|
2402
2441
|
if (pending.length > 0) {
|
|
@@ -2404,8 +2443,10 @@ export function runEligibility(sub, arg) {
|
|
|
2404
2443
|
` that provider and model on this account, then:\n` +
|
|
2405
2444
|
` llm-relay eligibility propose <n> --class <not-servable|subscription-required|allowance-exhausted|credential-invalid|rate-limited> \\\n` +
|
|
2406
2445
|
` --scope <attempt|group|deployment|credential|provider|model> [--members id1,id2] [--all-credentials] --rationale "..."\n` +
|
|
2407
|
-
` llm-relay eligibility accept <n> --class <...> --scope <...>\n` +
|
|
2408
|
-
` llm-relay eligibility reject <n>
|
|
2446
|
+
` llm-relay eligibility accept <n> --sig <digest> --class <...> --scope <...>\n` +
|
|
2447
|
+
` llm-relay eligibility reject <n> [--sig <digest>] # means nothing durable, and is remembered\n` +
|
|
2448
|
+
` --sig pins the item to its digest, so a queue that reorders between listing and accept\n` +
|
|
2449
|
+
` cannot land the verdict on a different refusal.\n` +
|
|
2409
2450
|
`\n Scope by what the message STATES, not by a pattern of failures: "credential" means the\n` +
|
|
2410
2451
|
` current credential slot; "provider" means all credentials for that provider. Groups stay\n` +
|
|
2411
2452
|
` on the current credential slot unless --all-credentials is stated explicitly.\n`);
|