usertrust 1.4.0 → 1.5.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/anomaly/detector.d.ts.map +1 -1
- package/dist/anomaly/detector.js +36 -11
- package/dist/anomaly/detector.js.map +1 -1
- package/dist/anomaly/signals/spend-velocity.d.ts +39 -12
- package/dist/anomaly/signals/spend-velocity.d.ts.map +1 -1
- package/dist/anomaly/signals/spend-velocity.js +66 -43
- package/dist/anomaly/signals/spend-velocity.js.map +1 -1
- package/dist/anomaly/signals/token-rate.d.ts +33 -13
- package/dist/anomaly/signals/token-rate.d.ts.map +1 -1
- package/dist/anomaly/signals/token-rate.js +124 -38
- package/dist/anomaly/signals/token-rate.js.map +1 -1
- package/dist/anomaly/types.d.ts +28 -6
- package/dist/anomaly/types.d.ts.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +98 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/main.js +0 -0
- package/dist/detect.d.ts +34 -1
- package/dist/detect.d.ts.map +1 -1
- package/dist/detect.js +124 -0
- package/dist/detect.js.map +1 -1
- package/dist/govern.d.ts +8 -1
- package/dist/govern.d.ts.map +1 -1
- package/dist/govern.js +191 -12
- package/dist/govern.js.map +1 -1
- package/dist/headless.d.ts +41 -2
- package/dist/headless.d.ts.map +1 -1
- package/dist/headless.js +54 -3
- package/dist/headless.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/ledger/pricing.d.ts +54 -0
- package/dist/ledger/pricing.d.ts.map +1 -1
- package/dist/ledger/pricing.js +128 -8
- package/dist/ledger/pricing.js.map +1 -1
- package/dist/shared/types.d.ts +175 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +55 -5
- package/dist/shared/types.js.map +1 -1
- package/dist/streaming.d.ts +8 -3
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js +61 -30
- package/dist/streaming.js.map +1 -1
- package/package.json +1 -1
package/dist/detect.js
CHANGED
|
@@ -47,4 +47,128 @@ export function detectClientKind(client) {
|
|
|
47
47
|
}
|
|
48
48
|
throw new Error("Unsupported LLM client: could not detect Anthropic, OpenAI, or Google SDK");
|
|
49
49
|
}
|
|
50
|
+
// ── Endpoint classification (M2 local-model governance) ──
|
|
51
|
+
// Compared against normalizeHostname(url.hostname) — the WHATWG-serialized,
|
|
52
|
+
// bracket-stripped form. `::ffff:7f00:1` is how Node's URL serializes the
|
|
53
|
+
// IPv4-mapped IPv6 loopback `::ffff:127.0.0.1` (the last 32 bits render as hex,
|
|
54
|
+
// never dotted-quad), so the map entry MUST be the serialized form to match.
|
|
55
|
+
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "::ffff:7f00:1"]);
|
|
56
|
+
/** Best-effort runtime hint by conventional default port (metadata only). */
|
|
57
|
+
const RUNTIME_BY_PORT = {
|
|
58
|
+
"11434": "ollama",
|
|
59
|
+
"1234": "lmstudio",
|
|
60
|
+
"8000": "vllm",
|
|
61
|
+
};
|
|
62
|
+
const CLOUD_DEFAULT = { class: "cloud", runtime: "unknown" };
|
|
63
|
+
function parseUrl(value) {
|
|
64
|
+
try {
|
|
65
|
+
return new URL(value);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Normalize a URL hostname for comparison: lowercase, IPv6 brackets stripped
|
|
73
|
+
* (Node's WHATWG URL keeps brackets in `hostname` for IPv6 literals).
|
|
74
|
+
*/
|
|
75
|
+
function normalizeHostname(hostname) {
|
|
76
|
+
const lower = hostname.toLowerCase();
|
|
77
|
+
if (lower.startsWith("[") && lower.endsWith("]"))
|
|
78
|
+
return lower.slice(1, -1);
|
|
79
|
+
return lower;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Match a parsed baseURL against one config.endpoints[].match entry (A1).
|
|
83
|
+
* Exactly three entry forms — never raw string prefixing:
|
|
84
|
+
* (i) scheme entry ("http://gpu-box:8000") → parsed; match iff origins are equal
|
|
85
|
+
* (path/query/trailing slash ignored; kills "http://gpu-box:8000.evil.com"
|
|
86
|
+
* and "http://gpu-box:8000@evil.com" bypass shapes);
|
|
87
|
+
* (ii) leading-star entry ("*.gpu.internal") → hostname SUFFIX match: matches
|
|
88
|
+
* "a.gpu.internal" and "x.y.gpu.internal", NOT "gpu.internal" itself;
|
|
89
|
+
* (iii) bare hostname → case-insensitive hostname equality (any port).
|
|
90
|
+
* Unparseable scheme entries never match (and never throw).
|
|
91
|
+
*/
|
|
92
|
+
function matchesEndpointEntry(entry, url) {
|
|
93
|
+
if (/^https?:\/\//i.test(entry)) {
|
|
94
|
+
const entryUrl = parseUrl(entry);
|
|
95
|
+
return entryUrl !== null && entryUrl.origin === url.origin;
|
|
96
|
+
}
|
|
97
|
+
const host = normalizeHostname(url.hostname);
|
|
98
|
+
if (entry.startsWith("*.")) {
|
|
99
|
+
const suffix = entry.slice(1).toLowerCase(); // ".gpu.internal"
|
|
100
|
+
return host.endsWith(suffix);
|
|
101
|
+
}
|
|
102
|
+
return host === entry.toLowerCase();
|
|
103
|
+
}
|
|
104
|
+
/** Read a string `baseURL` property off an SDK client instance, if present. */
|
|
105
|
+
function readBaseURL(client) {
|
|
106
|
+
if (client != null && typeof client === "object" && "baseURL" in client) {
|
|
107
|
+
const value = client.baseURL;
|
|
108
|
+
if (typeof value === "string")
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Classify the endpoint a client points at — local (self-hosted) or cloud —
|
|
115
|
+
* selecting the settlement regime for every governed call. Runs BESIDE
|
|
116
|
+
* `detectClientKind` (which stays the transport detector); the endpoint class,
|
|
117
|
+
* not the model string, picks local vs cloud metering, so model-name spoofing
|
|
118
|
+
* (`ollama cp llama3.2 gpt-4o`) cannot change the regime.
|
|
119
|
+
*
|
|
120
|
+
* Classification order:
|
|
121
|
+
* 1. `override.class` provided → override wins (runtime defaults to "unknown").
|
|
122
|
+
* 2. `client.baseURL` read when it is a string; absent or malformed (URL parse
|
|
123
|
+
* failure) → treated as absent → cloud default. Never throws.
|
|
124
|
+
* 3. `config.endpoints[]` first match (origin / leading-star hostname suffix /
|
|
125
|
+
* bare hostname — see matchesEndpointEntry).
|
|
126
|
+
* 4. Loopback autodetect (config.local.autoDetectLoopback): hostname
|
|
127
|
+
* localhost | 127.0.0.1 | ::1 | ::ffff:127.0.0.1 (case-insensitive, IPv6
|
|
128
|
+
* brackets stripped, IPv4-mapped form matched by its WHATWG serialization) →
|
|
129
|
+
* local, runtime hinted by port (11434 ollama, 1234 lmstudio, 8000 vllm,
|
|
130
|
+
* else "openai-compat").
|
|
131
|
+
* 5. Default `{ class: "cloud", runtime: "unknown" }` — fail-EXPENSIVE:
|
|
132
|
+
* over-charging at cloud rates is recoverable, under-charging a paid
|
|
133
|
+
* endpoint is not.
|
|
134
|
+
*
|
|
135
|
+
* **Security posture (trusted-operator boundary):** endpoint classification —
|
|
136
|
+
* config matchers, overrides, and loopback autodetect — is a TRUSTED-OPERATOR
|
|
137
|
+
* decision. Never wire it to end-user or request input. In server/multi-tenant
|
|
138
|
+
* deployments set `local.autoDetectLoopback: false` and classify via explicit
|
|
139
|
+
* `endpoints[]` config: loopback inside a container can be a forwarding sidecar
|
|
140
|
+
* to a paid API. This is the same trust boundary as `budget`/`customRates` —
|
|
141
|
+
* the config author already controls billing entirely. Note that a compromised
|
|
142
|
+
* local server can under-report usage; receipts expose `usageSource` and
|
|
143
|
+
* `meter.rateSource` precisely so that this is auditable.
|
|
144
|
+
*/
|
|
145
|
+
export function classifyEndpoint(client, config, override) {
|
|
146
|
+
const baseURL = readBaseURL(client);
|
|
147
|
+
// (1) Explicit override wins over everything.
|
|
148
|
+
if (override?.class !== undefined) {
|
|
149
|
+
const info = { class: override.class, runtime: override.runtime ?? "unknown" };
|
|
150
|
+
const overrideBase = override.baseURL ?? baseURL;
|
|
151
|
+
if (overrideBase !== undefined)
|
|
152
|
+
info.baseURL = overrideBase;
|
|
153
|
+
return info;
|
|
154
|
+
}
|
|
155
|
+
// (2) No readable baseURL → cloud default. Malformed → treat as absent, never throw.
|
|
156
|
+
if (baseURL === undefined)
|
|
157
|
+
return { ...CLOUD_DEFAULT };
|
|
158
|
+
const url = parseUrl(baseURL);
|
|
159
|
+
if (url === null)
|
|
160
|
+
return { ...CLOUD_DEFAULT };
|
|
161
|
+
// (3) config.endpoints[] — first match wins.
|
|
162
|
+
for (const entry of config.endpoints) {
|
|
163
|
+
if (matchesEndpointEntry(entry.match, url)) {
|
|
164
|
+
return { class: entry.class, runtime: entry.runtime, baseURL };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// (4) Loopback autodetect.
|
|
168
|
+
if (config.local.autoDetectLoopback && LOOPBACK_HOSTS.has(normalizeHostname(url.hostname))) {
|
|
169
|
+
return { class: "local", runtime: RUNTIME_BY_PORT[url.port] ?? "openai-compat", baseURL };
|
|
170
|
+
}
|
|
171
|
+
// (5) Cloud default.
|
|
172
|
+
return { class: "cloud", runtime: "unknown", baseURL };
|
|
173
|
+
}
|
|
50
174
|
//# sourceMappingURL=detect.js.map
|
package/dist/detect.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detect.js","sourceRoot":"","sources":["../src/detect.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,iCAAiC;AAuBjC;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAe;IAC/C,IACC,MAAM,IAAI,IAAI;QACd,OAAO,MAAM,KAAK,QAAQ;QAC1B,UAAU,IAAI,MAAM;QACpB,MAAM,CAAC,QAAQ,IAAI,IAAI;QACvB,OAAO,MAAM,CAAC,QAAQ,KAAK,QAAQ;QACnC,QAAQ,IAAI,MAAM,CAAC,QAAQ;QAC3B,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,KAAK,UAAU,EAC3C,CAAC;QACF,OAAO,WAAW,CAAC;IACpB,CAAC;IAED,IACC,MAAM,IAAI,IAAI;QACd,OAAO,MAAM,KAAK,QAAQ;QAC1B,MAAM,IAAI,MAAM;QAChB,MAAM,CAAC,IAAI,IAAI,IAAI;QACnB,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ;QAC/B,aAAa,IAAI,MAAM,CAAC,IAAI;QAC5B,MAAM,CAAC,IAAI,CAAC,WAAW,IAAI,IAAI;QAC/B,OAAO,MAAM,CAAC,IAAI,CAAC,WAAW,KAAK,QAAQ;QAC3C,QAAQ,IAAI,MAAM,CAAC,IAAI,CAAC,WAAW;QACnC,OAAO,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,KAAK,UAAU,EACnD,CAAC;QACF,OAAO,QAAQ,CAAC;IACjB,CAAC;IAED,IACC,MAAM,IAAI,IAAI;QACd,OAAO,MAAM,KAAK,QAAQ;QAC1B,QAAQ,IAAI,MAAM;QAClB,MAAM,CAAC,MAAM,IAAI,IAAI;QACrB,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ;QACjC,iBAAiB,IAAI,MAAM,CAAC,MAAM;QAClC,OAAO,MAAM,CAAC,MAAM,CAAC,eAAe,KAAK,UAAU,EAClD,CAAC;QACF,OAAO,QAAQ,CAAC;IACjB,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,2EAA2E,CAAC,CAAC;AAC9F,CAAC"}
|
|
1
|
+
{"version":3,"file":"detect.js","sourceRoot":"","sources":["../src/detect.ts"],"names":[],"mappings":"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"}
|
package/dist/govern.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AuditWriter } from "./audit/chain.js";
|
|
2
|
-
import type { ActionDescriptor, GovernedActionResult } from "./shared/types.js";
|
|
2
|
+
import type { ActionDescriptor, EndpointInfo, GovernedActionResult } from "./shared/types.js";
|
|
3
3
|
export interface TrustOpts {
|
|
4
4
|
/** Path to usertrust.config.json. Defaults to `.usertrust/usertrust.config.json`. */
|
|
5
5
|
configPath?: string;
|
|
@@ -25,6 +25,13 @@ export interface TrustOpts {
|
|
|
25
25
|
dryRun?: boolean;
|
|
26
26
|
/** Vault directory override (default: cwd). */
|
|
27
27
|
vaultBase?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Explicit endpoint classification override (M2) — wins over config.endpoints
|
|
30
|
+
* matchers and loopback autodetect. TRUSTED-OPERATOR input, same trust
|
|
31
|
+
* boundary as budget/customRates: never derive it from end-user or request
|
|
32
|
+
* data (A10).
|
|
33
|
+
*/
|
|
34
|
+
endpoint?: Partial<EndpointInfo> | undefined;
|
|
28
35
|
/**
|
|
29
36
|
* Inject a mock/test engine. When set, used instead of TigerBeetle.
|
|
30
37
|
* Primarily for testing failure modes.
|
package/dist/govern.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"govern.d.ts","sourceRoot":"","sources":["../src/govern.ts"],"names":[],"mappings":"AAmCA,OAAO,EAAE,KAAK,WAAW,EAAqB,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"govern.d.ts","sourceRoot":"","sources":["../src/govern.ts"],"names":[],"mappings":"AAmCA,OAAO,EAAE,KAAK,WAAW,EAAqB,MAAM,kBAAkB,CAAC;AAgCvE,OAAO,KAAK,EACX,gBAAgB,EAChB,YAAY,EACZ,oBAAoB,EAKpB,MAAM,mBAAmB,CAAC;AAK3B,MAAM,WAAW,SAAS;IACzB,qFAAqF;IACrF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qBAAqB;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,SAAS,CAAC;IAC7C;;;;OAIG;IACH,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC7B;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,8DAA8D;AAC9D,MAAM,WAAW,WAAW;IAC3B,YAAY,CAAC,MAAM,EAAE;QACpB,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpC;;;OAGG;IACH,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3E,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,gEAAgE;IAChE,cAAc,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,OAAO,CAAC,IAAI,IAAI,CAAC;CACjB;AAED,sEAAsE;AACtE,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG;IAClC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,YAAY,CAAC,CAAC,EACb,MAAM,EAAE,gBAAgB,EACxB,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACvB,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC;CACpC,CAAC;AA6KF,wBAAsB,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CA2xCrF"}
|
package/dist/govern.js
CHANGED
|
@@ -33,9 +33,9 @@ import { join } from "node:path";
|
|
|
33
33
|
import { CreateTransferError } from "tigerbeetle-node";
|
|
34
34
|
import { createAuditWriter } from "./audit/chain.js";
|
|
35
35
|
import { writeReceipt } from "./audit/rotation.js";
|
|
36
|
-
import { detectClientKind } from "./detect.js";
|
|
36
|
+
import { classifyEndpoint, detectClientKind } from "./detect.js";
|
|
37
37
|
import { TBTransferError, TrustTBClient, XFER_SPEND } from "./ledger/client.js";
|
|
38
|
-
import {
|
|
38
|
+
import { costFromRates, estimateInputTokens, resolveRates, warnUnknownModel, } from "./ledger/pricing.js";
|
|
39
39
|
import { recordPattern } from "./memory/patterns.js";
|
|
40
40
|
import { DEFAULT_RULES, mergePolicies } from "./policy/default-rules.js";
|
|
41
41
|
import { evaluatePolicy, loadPolicies } from "./policy/gate.js";
|
|
@@ -66,6 +66,86 @@ class AsyncMutex {
|
|
|
66
66
|
return release;
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
+
// ── M2 unknown-model policy (A5) ──
|
|
70
|
+
/** USD value of one usertoken: 1 usertoken = $0.0001 (one basis point of a cent). */
|
|
71
|
+
const USERTOKENS_PER_DOLLAR = 10_000;
|
|
72
|
+
/**
|
|
73
|
+
* Enforce config.unknownModelPolicy at AUTHORIZE time (A5). Only cloud-scope
|
|
74
|
+
* resolutions can be unknown — local scope always resolves to local rates.
|
|
75
|
+
* "deny" throws before any PENDING hold; "warn" logs once per model string per
|
|
76
|
+
* process via the shared warnUnknownModel helper (receipts carry
|
|
77
|
+
* meter.rateSource "fallback" regardless of warn dedup); "fallback" is silent.
|
|
78
|
+
*/
|
|
79
|
+
function enforceUnknownModelPolicy(model, resolution, config) {
|
|
80
|
+
if (!resolution.unknown)
|
|
81
|
+
return;
|
|
82
|
+
if (config.unknownModelPolicy === "deny") {
|
|
83
|
+
throw new PolicyDeniedError(`unknown_model: ${model} not in pricing table`);
|
|
84
|
+
}
|
|
85
|
+
if (config.unknownModelPolicy === "warn") {
|
|
86
|
+
warnUnknownModel(model);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A4: build forward-args for a local openai stream call with
|
|
91
|
+
* stream_options.include_usage merged in. The caller's other stream_options
|
|
92
|
+
* fields survive; an explicit include_usage (true OR false) is respected —
|
|
93
|
+
* only a missing/null value injects. Returns null when no injection applies.
|
|
94
|
+
*/
|
|
95
|
+
function withInjectedUsageOptions(args) {
|
|
96
|
+
const params = (args[0] ?? {});
|
|
97
|
+
if (params.stream !== true)
|
|
98
|
+
return null;
|
|
99
|
+
const rawOptions = params.stream_options;
|
|
100
|
+
const streamOptions = rawOptions != null && typeof rawOptions === "object"
|
|
101
|
+
? rawOptions
|
|
102
|
+
: undefined;
|
|
103
|
+
if (streamOptions?.include_usage != null)
|
|
104
|
+
return null;
|
|
105
|
+
return [
|
|
106
|
+
{ ...params, stream_options: { ...streamOptions, include_usage: true } },
|
|
107
|
+
...args.slice(1),
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
/** Message shapes an OpenAI-compat server emits when it rejects an unknown field. */
|
|
111
|
+
const STREAM_OPTIONS_REJECTION_RE = /stream_options|include_usage|unrecognized|unknown.{0,20}(field|argument|parameter|option)/i;
|
|
112
|
+
/**
|
|
113
|
+
* A4 retry heuristic: does `err` plausibly indicate that the server rejected the
|
|
114
|
+
* injected `stream_options.include_usage`? We only retry-without-injection for
|
|
115
|
+
* these — a blanket retry on ANY error would double the provider call on
|
|
116
|
+
* transient failures (ECONNRESET, timeouts, 5xx) and mask the real root cause,
|
|
117
|
+
* so everything else rethrows the ORIGINAL error immediately. Matches the
|
|
118
|
+
* rejection message on the error, its nested `error.message`, or a cheaply
|
|
119
|
+
* reachable `response` body; or an HTTP-shaped 400/422 status on the error
|
|
120
|
+
* object. Tradeoff: a server that rejects the field with an opaque 500 and no
|
|
121
|
+
* telltale text is NOT retried — acceptable, since include_usage is widely
|
|
122
|
+
* supported and the caller still receives the original error.
|
|
123
|
+
*/
|
|
124
|
+
function looksLikeStreamOptionsRejection(err) {
|
|
125
|
+
if (err == null || typeof err !== "object") {
|
|
126
|
+
return err instanceof Error && STREAM_OPTIONS_REJECTION_RE.test(err.message);
|
|
127
|
+
}
|
|
128
|
+
const e = err;
|
|
129
|
+
const status = e.status ?? e.statusCode;
|
|
130
|
+
if (status === 400 || status === 422)
|
|
131
|
+
return true;
|
|
132
|
+
const candidates = [e.message, e.body];
|
|
133
|
+
if (err instanceof Error)
|
|
134
|
+
candidates.push(err.message);
|
|
135
|
+
const nested = e.error;
|
|
136
|
+
if (nested != null && typeof nested === "object") {
|
|
137
|
+
candidates.push(nested.message);
|
|
138
|
+
}
|
|
139
|
+
const response = e.response;
|
|
140
|
+
if (typeof response === "string") {
|
|
141
|
+
candidates.push(response);
|
|
142
|
+
}
|
|
143
|
+
else if (response != null && typeof response === "object") {
|
|
144
|
+
const r = response;
|
|
145
|
+
candidates.push(r.data, r.body);
|
|
146
|
+
}
|
|
147
|
+
return candidates.some((c) => typeof c === "string" && STREAM_OPTIONS_REJECTION_RE.test(c));
|
|
148
|
+
}
|
|
69
149
|
async function loadSpendLedger(vaultBase) {
|
|
70
150
|
const ledgerPath = join(vaultBase, VAULT_DIR, "spend-ledger.json");
|
|
71
151
|
try {
|
|
@@ -139,7 +219,6 @@ export async function trust(client, opts) {
|
|
|
139
219
|
budget: opts?.budget ?? DEFAULT_BUDGET,
|
|
140
220
|
});
|
|
141
221
|
}
|
|
142
|
-
const customRates = config.pricing === "custom" ? config.customRates : undefined;
|
|
143
222
|
const isDryRun = opts?.dryRun ?? process.env.USERTRUST_DRY_RUN === "true";
|
|
144
223
|
// AUD-470: Only accept injected _engine/_audit in test environments.
|
|
145
224
|
// In production, silently ignore them to prevent governance bypass.
|
|
@@ -189,8 +268,12 @@ export async function trust(client, opts) {
|
|
|
189
268
|
else {
|
|
190
269
|
engine = null;
|
|
191
270
|
}
|
|
192
|
-
// 5. Detect client kind
|
|
271
|
+
// 5. Detect client kind (transport) + classify the endpoint (settlement
|
|
272
|
+
// regime, M2). classifyEndpoint runs BESIDE detectClientKind — the endpoint
|
|
273
|
+
// class, not the model string, picks local vs cloud metering (A3: this scope
|
|
274
|
+
// is captured once here and used verbatim at every authorize/settle below).
|
|
193
275
|
const kind = detectClientKind(client);
|
|
276
|
+
const endpoint = classifyEndpoint(client, config, opts?.endpoint);
|
|
194
277
|
// 6. Track state
|
|
195
278
|
let destroyed = false;
|
|
196
279
|
const budgetMutex = new AsyncMutex(); // AUD-453: serialise budget-check + hold
|
|
@@ -199,7 +282,29 @@ export async function trust(client, opts) {
|
|
|
199
282
|
let inFlightHoldTotal = 0; // Track estimated cost of in-flight pending holds
|
|
200
283
|
// Streaming anomaly detector — shared across calls so injection-cascade
|
|
201
284
|
// can track signals across the conversation. Disabled when config.anomaly.enabled=false.
|
|
202
|
-
|
|
285
|
+
// M2 seam fix: the injected costCalculator prices spend-velocity with the
|
|
286
|
+
// SAME scoped rates as settlement, using the observed event's
|
|
287
|
+
// model/endpointClass (the detector is shared while the model varies per
|
|
288
|
+
// call). Denominations differ by design: cloud events → DOLLARS against
|
|
289
|
+
// thresholdDollarsPerMin; local events → nominal usertokens against
|
|
290
|
+
// localThresholdUsertokensPerMin, WITHOUT the per-call >=1 settlement floor.
|
|
291
|
+
// Settlement floors per call; the anomaly signal measures flow — so a
|
|
292
|
+
// default {0,0}-rate local stream contributes 0 and can never false-trip
|
|
293
|
+
// spend_velocity (rejected-merge design note, pinned by Task 3 tests).
|
|
294
|
+
const anomalyDetector = createAnomalyDetector(config.anomaly, {
|
|
295
|
+
provider: kind,
|
|
296
|
+
costCalculator: (calcModel, inputTokens, outputTokens, event) => {
|
|
297
|
+
const scope = event?.endpointClass ?? "cloud";
|
|
298
|
+
const resolution = resolveRates(event?.model ?? calcModel, scope, config);
|
|
299
|
+
if (scope === "local") {
|
|
300
|
+
const inTok = Number.isFinite(inputTokens) && inputTokens > 0 ? inputTokens : 0;
|
|
301
|
+
const outTok = Number.isFinite(outputTokens) && outputTokens > 0 ? outputTokens : 0;
|
|
302
|
+
return ((inTok / 1000) * resolution.rates.inputPer1k +
|
|
303
|
+
(outTok / 1000) * resolution.rates.outputPer1k);
|
|
304
|
+
}
|
|
305
|
+
return costFromRates(resolution.rates, inputTokens, outputTokens) / USERTOKENS_PER_DOLLAR;
|
|
306
|
+
},
|
|
307
|
+
});
|
|
203
308
|
// 7. Two-phase intercept
|
|
204
309
|
async function interceptCall(originalFn, thisArg, args) {
|
|
205
310
|
if (destroyed) {
|
|
@@ -224,11 +329,17 @@ export async function trust(client, opts) {
|
|
|
224
329
|
// a. Circuit breaker check
|
|
225
330
|
const cb = breaker.get(kind);
|
|
226
331
|
cb.allowRequest();
|
|
227
|
-
// b. Estimate cost (before policy, so cost fields are available in context)
|
|
332
|
+
// b. Estimate cost (before policy, so cost fields are available in context).
|
|
333
|
+
// M2: rates resolve within the endpoint scope CAPTURED AT AUTHORIZE (A3) —
|
|
334
|
+
// this one resolution prices the hold, both settlement paths, and the
|
|
335
|
+
// receipt's meter provenance. unknownModelPolicy is enforced here, at
|
|
336
|
+
// authorize time, before any PENDING hold (A5).
|
|
228
337
|
const transferId = trustId("tx");
|
|
229
338
|
const estimatedInputTokens = estimateInputTokens(promptParts);
|
|
230
339
|
const maxOutputTokens = params.max_tokens ?? 4096;
|
|
231
|
-
const
|
|
340
|
+
const rateResolution = resolveRates(model, endpoint.class, config);
|
|
341
|
+
enforceUnknownModelPolicy(model, rateResolution, config);
|
|
342
|
+
const estimatedCost = costFromRates(rateResolution.rates, estimatedInputTokens, maxOutputTokens);
|
|
232
343
|
// AUD-453: Acquire mutex to serialise budget-check + PENDING hold.
|
|
233
344
|
// This prevents concurrent calls from both passing the budget check
|
|
234
345
|
// and overshooting the budget.
|
|
@@ -352,11 +463,41 @@ export async function trust(client, opts) {
|
|
|
352
463
|
// AUD-453: Release lock after budget check + hold are complete
|
|
353
464
|
releaseBudgetLock();
|
|
354
465
|
}
|
|
466
|
+
// e0. M2 usage injection (A4): for local openai streams, opt in to the
|
|
467
|
+
// server's final usage chunk (Ollama emits /v1 streaming usage ONLY when
|
|
468
|
+
// stream_options.include_usage is set). The merge preserves the caller's
|
|
469
|
+
// other stream_options fields and respects an explicit include_usage.
|
|
470
|
+
// The resulting usage chunk is FORWARDED to the consumer unmodified
|
|
471
|
+
// (transparent middleware).
|
|
472
|
+
let preInjectionArgs = null;
|
|
473
|
+
if (kind === "openai" && endpoint.class === "local" && config.local.injectUsageOptions) {
|
|
474
|
+
const injected = withInjectedUsageOptions(forwardArgs);
|
|
475
|
+
if (injected != null) {
|
|
476
|
+
preInjectionArgs = forwardArgs;
|
|
477
|
+
forwardArgs = injected;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
355
480
|
// e. Forward to original SDK. P3-PII-REDACT-EGRESS: forwardArgs is the
|
|
356
481
|
// redacted clone in redact mode, or the original args otherwise.
|
|
357
482
|
let settled = true;
|
|
358
483
|
try {
|
|
359
|
-
|
|
484
|
+
let response;
|
|
485
|
+
try {
|
|
486
|
+
response = await originalFn.apply(thisArg, forwardArgs);
|
|
487
|
+
}
|
|
488
|
+
catch (callErr) {
|
|
489
|
+
// A4: some OpenAI-compat servers reject unknown stream_options. When
|
|
490
|
+
// WE injected them AND the error plausibly says so (message/HTTP-status
|
|
491
|
+
// heuristic), retry ONCE without the injection; the retried stream
|
|
492
|
+
// simply settles on the estimate if no usage tail arrives (A7). Any
|
|
493
|
+
// other error — transient network failures, unrelated 5xx — rethrows
|
|
494
|
+
// the ORIGINAL immediately rather than duplicating compute and masking
|
|
495
|
+
// the root cause.
|
|
496
|
+
if (preInjectionArgs == null || !looksLikeStreamOptionsRejection(callErr)) {
|
|
497
|
+
throw callErr;
|
|
498
|
+
}
|
|
499
|
+
response = await originalFn.apply(thisArg, preInjectionArgs);
|
|
500
|
+
}
|
|
360
501
|
// e2. Streaming detection: if response is an async iterable, wrap with
|
|
361
502
|
// token accumulation. Settlement and audit happen when the stream ends.
|
|
362
503
|
if (response != null &&
|
|
@@ -364,11 +505,13 @@ export async function trust(client, opts) {
|
|
|
364
505
|
Symbol.asyncIterator in response) {
|
|
365
506
|
const stream = response;
|
|
366
507
|
const governedStream = createGovernedStream(stream, kind, async (completion) => {
|
|
367
|
-
// Determine cost: use provider usage if reported, else fall back to
|
|
508
|
+
// Determine cost: use provider usage if reported, else fall back to
|
|
509
|
+
// estimate. A3: priced with the rate resolution captured at
|
|
510
|
+
// authorize; A11: costFromRates floors at 1 even for 0/0 usage.
|
|
368
511
|
let streamCost;
|
|
369
512
|
let usageSource;
|
|
370
513
|
if (completion.usageReported) {
|
|
371
|
-
streamCost =
|
|
514
|
+
streamCost = costFromRates(rateResolution.rates, completion.usage.inputTokens, completion.usage.outputTokens);
|
|
372
515
|
usageSource = "provider";
|
|
373
516
|
}
|
|
374
517
|
else {
|
|
@@ -455,6 +598,10 @@ export async function trust(client, opts) {
|
|
|
455
598
|
transferId,
|
|
456
599
|
usageSource,
|
|
457
600
|
chunksDelivered: completion.chunksDelivered,
|
|
601
|
+
// M2: metering provenance mirrors the receipt (A3 authorize-time scope).
|
|
602
|
+
endpointClass: endpoint.class,
|
|
603
|
+
costBasis: rateResolution.costBasis,
|
|
604
|
+
rateSource: rateResolution.rateSource,
|
|
458
605
|
};
|
|
459
606
|
if (config.pii === "warn" || config.pii === "redact") {
|
|
460
607
|
const piiResult = redactPII(promptParts);
|
|
@@ -493,6 +640,12 @@ export async function trust(client, opts) {
|
|
|
493
640
|
timestamp: new Date().toISOString(),
|
|
494
641
|
usageSource,
|
|
495
642
|
chunksDelivered: completion.chunksDelivered,
|
|
643
|
+
// M2 provenance (A6: computeMs omitted — no compute-time source here).
|
|
644
|
+
endpoint: { class: endpoint.class, runtime: endpoint.runtime },
|
|
645
|
+
meter: {
|
|
646
|
+
costBasis: rateResolution.costBasis,
|
|
647
|
+
rateSource: rateResolution.rateSource,
|
|
648
|
+
},
|
|
496
649
|
...(callAuditDegraded ? { auditDegraded: true } : {}),
|
|
497
650
|
// AUD-456: Flag proxy stub receipts
|
|
498
651
|
...(proxyConn != null ? { proxyStub: true } : {}),
|
|
@@ -546,6 +699,10 @@ export async function trust(client, opts) {
|
|
|
546
699
|
deltaTokens: obs.deltaTokens,
|
|
547
700
|
cumulativeInputTokens: obs.cumulativeInputTokens,
|
|
548
701
|
cumulativeOutputTokens: obs.cumulativeOutputTokens,
|
|
702
|
+
// M2: stamp the per-call scope so the SHARED detector prices
|
|
703
|
+
// this event with the same scoped rates as settlement.
|
|
704
|
+
model,
|
|
705
|
+
endpointClass: endpoint.class,
|
|
549
706
|
});
|
|
550
707
|
const verdict = anomalyDetector.check();
|
|
551
708
|
if (verdict.tripped) {
|
|
@@ -584,6 +741,12 @@ export async function trust(client, opts) {
|
|
|
584
741
|
model,
|
|
585
742
|
provider: kind,
|
|
586
743
|
timestamp: new Date().toISOString(),
|
|
744
|
+
// M2: authorize-time scope, already fixed for the eventual settle (A3).
|
|
745
|
+
endpoint: { class: endpoint.class, runtime: endpoint.runtime },
|
|
746
|
+
meter: {
|
|
747
|
+
costBasis: rateResolution.costBasis,
|
|
748
|
+
rateSource: rateResolution.rateSource,
|
|
749
|
+
},
|
|
587
750
|
...(callAuditDegraded ? { auditDegraded: true } : {}),
|
|
588
751
|
// AUD-456: Flag proxy stub receipts
|
|
589
752
|
...(proxyConn != null ? { proxyStub: true } : {}),
|
|
@@ -591,8 +754,11 @@ export async function trust(client, opts) {
|
|
|
591
754
|
inFlightStreamCount++;
|
|
592
755
|
return { response: governedStream, receipt: estimatedReceipt };
|
|
593
756
|
}
|
|
594
|
-
// f. Compute actual cost from response usage
|
|
757
|
+
// f. Compute actual cost from response usage. A3: priced with the rate
|
|
758
|
+
// resolution captured at authorize. costFromRates clamps NaN/negative
|
|
759
|
+
// counts (A7) and floors at 1 even for 0/0 provider usage (A11).
|
|
595
760
|
let actualCost = estimatedCost;
|
|
761
|
+
let usageSource = "estimated";
|
|
596
762
|
if (response != null && typeof response === "object" && "usage" in response) {
|
|
597
763
|
const usage = response.usage;
|
|
598
764
|
if (usage != null) {
|
|
@@ -602,7 +768,8 @@ export async function trust(client, opts) {
|
|
|
602
768
|
const outputTokens = usage.output_tokens ??
|
|
603
769
|
usage.completion_tokens ??
|
|
604
770
|
0;
|
|
605
|
-
actualCost =
|
|
771
|
+
actualCost = costFromRates(rateResolution.rates, inputTokens, outputTokens);
|
|
772
|
+
usageSource = "provider";
|
|
606
773
|
}
|
|
607
774
|
}
|
|
608
775
|
// h. Audit the llm_call FIRST (P3-AUDIT-FAILCLOSED). The settlement-
|
|
@@ -619,6 +786,11 @@ export async function trust(client, opts) {
|
|
|
619
786
|
cost: actualCost,
|
|
620
787
|
settled: true,
|
|
621
788
|
transferId,
|
|
789
|
+
usageSource,
|
|
790
|
+
// M2: metering provenance mirrors the receipt (A3 authorize-time scope).
|
|
791
|
+
endpointClass: endpoint.class,
|
|
792
|
+
costBasis: rateResolution.costBasis,
|
|
793
|
+
rateSource: rateResolution.rateSource,
|
|
622
794
|
};
|
|
623
795
|
if (config.pii === "warn" || config.pii === "redact") {
|
|
624
796
|
const piiResult = redactPII(promptParts);
|
|
@@ -750,6 +922,13 @@ export async function trust(client, opts) {
|
|
|
750
922
|
model,
|
|
751
923
|
provider: kind,
|
|
752
924
|
timestamp: new Date().toISOString(),
|
|
925
|
+
usageSource,
|
|
926
|
+
// M2 provenance (A6: computeMs omitted — no compute-time source here).
|
|
927
|
+
endpoint: { class: endpoint.class, runtime: endpoint.runtime },
|
|
928
|
+
meter: {
|
|
929
|
+
costBasis: rateResolution.costBasis,
|
|
930
|
+
rateSource: rateResolution.rateSource,
|
|
931
|
+
},
|
|
753
932
|
...(callAuditDegraded ? { auditDegraded: true } : {}),
|
|
754
933
|
// AUD-456: Flag proxy stub receipts
|
|
755
934
|
...(proxyConn != null ? { proxyStub: true } : {}),
|