@trazum/core 1.65.0 → 1.66.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/config-schema.d.ts +32 -1
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config-schema.js +55 -3
- package/dist/config-schema.js.map +1 -1
- package/dist/gateway.d.ts +32 -1
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +64 -3
- package/dist/gateway.js.map +1 -1
- package/dist/guard.d.ts +20 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +41 -3
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/judgement.d.ts +159 -0
- package/dist/judgement.d.ts.map +1 -0
- package/dist/judgement.js +188 -0
- package/dist/judgement.js.map +1 -0
- package/dist/position.d.ts +74 -0
- package/dist/position.d.ts.map +1 -0
- package/dist/position.js +93 -0
- package/dist/position.js.map +1 -0
- package/dist/session-cost.d.ts +2 -0
- package/dist/session-cost.d.ts.map +1 -1
- package/dist/session-cost.js +1 -1
- package/dist/session-cost.js.map +1 -1
- package/package.json +1 -1
- package/src/config-schema.ts +89 -3
- package/src/gateway.ts +108 -4
- package/src/guard.ts +73 -3
- package/src/index.ts +16 -1
- package/src/judgement.ts +346 -0
- package/src/position.ts +126 -0
- package/src/session-cost.ts +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The measured side of the limits policy: what a usage log says each scope
|
|
3
|
+
* has already spent.
|
|
4
|
+
*
|
|
5
|
+
* `judgeLimits` judges; this measures. The split is the product's oldest
|
|
6
|
+
* rule — the judging function must be pure and instant, so whatever reads
|
|
7
|
+
* and prices records happens once, here, and the doors keep the result.
|
|
8
|
+
*
|
|
9
|
+
* **Absence is `null`, never zero.** A log with no clock cannot say what
|
|
10
|
+
* today has cost; a log that records no sessions cannot say what a session
|
|
11
|
+
* has spent. Both come back `null`, and `judgeLimits` turns a `null` into
|
|
12
|
+
* `cannot-tell` rather than an approval. The one deliberate asymmetry: a log
|
|
13
|
+
* that *does* record sessions and has never seen this one answers `0` — the
|
|
14
|
+
* history of a conversation that has not started is complete and empty,
|
|
15
|
+
* which is a measurement, not an absence.
|
|
16
|
+
*
|
|
17
|
+
* **Session keys never leave this module.** The index groups by them — that
|
|
18
|
+
* is the whole job — but the map lives in door memory, `positionAt` returns
|
|
19
|
+
* only dollar figures, and nothing here is serialised. The guarantee the
|
|
20
|
+
* usage module states (a session key is somebody's conversation and is never
|
|
21
|
+
* printed) holds through this door too.
|
|
22
|
+
*/
|
|
23
|
+
import type { UsageRecord } from './usage.js';
|
|
24
|
+
import type { PricingCatalogue } from './pricing.js';
|
|
25
|
+
import type { MeasuredPosition } from './judgement.js';
|
|
26
|
+
/**
|
|
27
|
+
* Everything a door needs to answer `positionAt` in constant time, built
|
|
28
|
+
* once from the log it was pointed at.
|
|
29
|
+
*/
|
|
30
|
+
export interface MeasuredIndex {
|
|
31
|
+
/** Spend inside the current UTC day. Null when no record carries a clock. */
|
|
32
|
+
dayUsd: number | null;
|
|
33
|
+
/** The UTC day the figure covers. Null exactly when `dayUsd` is. */
|
|
34
|
+
dayWindow: {
|
|
35
|
+
fromMs: number;
|
|
36
|
+
toMs: number;
|
|
37
|
+
} | null;
|
|
38
|
+
/** Whether any record carried a label / a session at all. */
|
|
39
|
+
labelsSeen: boolean;
|
|
40
|
+
sessionsSeen: boolean;
|
|
41
|
+
/** Priced spend per label. Keys are labels, which do print. */
|
|
42
|
+
labels: ReadonlyMap<string, number>;
|
|
43
|
+
/** Priced spend per session. Keys are session identifiers — never printed. */
|
|
44
|
+
sessions: ReadonlyMap<string, number>;
|
|
45
|
+
/** Whether the log held any records at all. */
|
|
46
|
+
any: boolean;
|
|
47
|
+
/** Records whose model the catalogue cannot price — money nobody can see. */
|
|
48
|
+
unpriced: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Prices and groups a parsed usage log, once.
|
|
52
|
+
*
|
|
53
|
+
* An unpriced record contributes nothing to any figure and is **counted**
|
|
54
|
+
* instead of dropped silently: a door that reports "measured $3" over a log
|
|
55
|
+
* with a thousand unpriced records is wrong by an amount nobody can see, in
|
|
56
|
+
* the flattering direction, and `unpriced` is how the door says so.
|
|
57
|
+
*/
|
|
58
|
+
export declare function indexUsage(records: readonly UsageRecord[], options: {
|
|
59
|
+
catalogue: PricingCatalogue;
|
|
60
|
+
on?: Date;
|
|
61
|
+
}): MeasuredIndex;
|
|
62
|
+
/**
|
|
63
|
+
* The measured position for one proposed call, from an index built once.
|
|
64
|
+
*
|
|
65
|
+
* A scope the log cannot see is `null`; a scope it can see and has nothing
|
|
66
|
+
* recorded for is `0`. The difference is the difference between "unknown"
|
|
67
|
+
* and "a conversation that has not started yet", and collapsing them is how
|
|
68
|
+
* a dead log approves a live spend.
|
|
69
|
+
*/
|
|
70
|
+
export declare function positionAt(index: MeasuredIndex, call: {
|
|
71
|
+
label?: string;
|
|
72
|
+
session?: string;
|
|
73
|
+
}): MeasuredPosition;
|
|
74
|
+
//# sourceMappingURL=position.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"position.d.ts","sourceRoot":"","sources":["../src/position.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAIvD;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,oEAAoE;IACpE,SAAS,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACnD,6DAA6D;IAC7D,UAAU,EAAE,OAAO,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;IACtB,+DAA+D;IAC/D,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,8EAA8E;IAC9E,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,+CAA+C;IAC/C,GAAG,EAAE,OAAO,CAAC;IACb,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,SAAS,WAAW,EAAE,EAC/B,OAAO,EAAE;IAAE,SAAS,EAAE,gBAAgB,CAAC;IAAC,EAAE,CAAC,EAAE,IAAI,CAAA;CAAE,GAClD,aAAa,CAsCf;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,aAAa,EACpB,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GACzC,gBAAgB,CAWlB"}
|
package/dist/position.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The measured side of the limits policy: what a usage log says each scope
|
|
3
|
+
* has already spent.
|
|
4
|
+
*
|
|
5
|
+
* `judgeLimits` judges; this measures. The split is the product's oldest
|
|
6
|
+
* rule — the judging function must be pure and instant, so whatever reads
|
|
7
|
+
* and prices records happens once, here, and the doors keep the result.
|
|
8
|
+
*
|
|
9
|
+
* **Absence is `null`, never zero.** A log with no clock cannot say what
|
|
10
|
+
* today has cost; a log that records no sessions cannot say what a session
|
|
11
|
+
* has spent. Both come back `null`, and `judgeLimits` turns a `null` into
|
|
12
|
+
* `cannot-tell` rather than an approval. The one deliberate asymmetry: a log
|
|
13
|
+
* that *does* record sessions and has never seen this one answers `0` — the
|
|
14
|
+
* history of a conversation that has not started is complete and empty,
|
|
15
|
+
* which is a measurement, not an absence.
|
|
16
|
+
*
|
|
17
|
+
* **Session keys never leave this module.** The index groups by them — that
|
|
18
|
+
* is the whole job — but the map lives in door memory, `positionAt` returns
|
|
19
|
+
* only dollar figures, and nothing here is serialised. The guarantee the
|
|
20
|
+
* usage module states (a session key is somebody's conversation and is never
|
|
21
|
+
* printed) holds through this door too.
|
|
22
|
+
*/
|
|
23
|
+
import { costOf } from './session-cost.js';
|
|
24
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
25
|
+
/**
|
|
26
|
+
* Prices and groups a parsed usage log, once.
|
|
27
|
+
*
|
|
28
|
+
* An unpriced record contributes nothing to any figure and is **counted**
|
|
29
|
+
* instead of dropped silently: a door that reports "measured $3" over a log
|
|
30
|
+
* with a thousand unpriced records is wrong by an amount nobody can see, in
|
|
31
|
+
* the flattering direction, and `unpriced` is how the door says so.
|
|
32
|
+
*/
|
|
33
|
+
export function indexUsage(records, options) {
|
|
34
|
+
const { catalogue, on = new Date() } = options;
|
|
35
|
+
const dayFromMs = Math.floor(on.getTime() / DAY_MS) * DAY_MS;
|
|
36
|
+
const dayToMs = dayFromMs + DAY_MS;
|
|
37
|
+
const labels = new Map();
|
|
38
|
+
const sessions = new Map();
|
|
39
|
+
let labelsSeen = false;
|
|
40
|
+
let sessionsSeen = false;
|
|
41
|
+
let clocksSeen = false;
|
|
42
|
+
let dayUsd = 0;
|
|
43
|
+
let unpriced = 0;
|
|
44
|
+
for (const record of records) {
|
|
45
|
+
if (record.label !== null)
|
|
46
|
+
labelsSeen = true;
|
|
47
|
+
if (record.session !== null)
|
|
48
|
+
sessionsSeen = true;
|
|
49
|
+
if (record.ts !== null)
|
|
50
|
+
clocksSeen = true;
|
|
51
|
+
const usd = costOf(record, catalogue, on);
|
|
52
|
+
if (usd === null) {
|
|
53
|
+
unpriced += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (record.label !== null)
|
|
57
|
+
labels.set(record.label, (labels.get(record.label) ?? 0) + usd);
|
|
58
|
+
if (record.session !== null)
|
|
59
|
+
sessions.set(record.session, (sessions.get(record.session) ?? 0) + usd);
|
|
60
|
+
if (record.ts !== null && record.ts >= dayFromMs && record.ts < dayToMs)
|
|
61
|
+
dayUsd += usd;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
dayUsd: clocksSeen ? dayUsd : null,
|
|
65
|
+
dayWindow: clocksSeen ? { fromMs: dayFromMs, toMs: dayToMs } : null,
|
|
66
|
+
labelsSeen,
|
|
67
|
+
sessionsSeen,
|
|
68
|
+
labels,
|
|
69
|
+
sessions,
|
|
70
|
+
any: records.length > 0,
|
|
71
|
+
unpriced,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The measured position for one proposed call, from an index built once.
|
|
76
|
+
*
|
|
77
|
+
* A scope the log cannot see is `null`; a scope it can see and has nothing
|
|
78
|
+
* recorded for is `0`. The difference is the difference between "unknown"
|
|
79
|
+
* and "a conversation that has not started yet", and collapsing them is how
|
|
80
|
+
* a dead log approves a live spend.
|
|
81
|
+
*/
|
|
82
|
+
export function positionAt(index, call) {
|
|
83
|
+
if (!index.any) {
|
|
84
|
+
return { dayUsd: null, sessionUsd: null, labelUsd: null };
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
dayUsd: index.dayUsd,
|
|
88
|
+
dayWindow: index.dayWindow,
|
|
89
|
+
sessionUsd: call.session !== undefined && index.sessionsSeen ? (index.sessions.get(call.session) ?? 0) : null,
|
|
90
|
+
labelUsd: call.label !== undefined && index.labelsSeen ? (index.labels.get(call.label) ?? 0) : null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=position.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"position.js","sourceRoot":"","sources":["../src/position.ts"],"names":[],"mappings":"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"}
|
package/dist/session-cost.d.ts
CHANGED
|
@@ -64,6 +64,8 @@ export interface SessionCostTracker {
|
|
|
64
64
|
add(record: UsageRecord): void;
|
|
65
65
|
finish(): SessionCostShape[];
|
|
66
66
|
}
|
|
67
|
+
/** Every billed dollar of one call, at its own model's rates. */
|
|
68
|
+
export declare function costOf(record: UsageRecord, catalogue: PricingCatalogue, on: Date): number | null;
|
|
67
69
|
export declare function createSessionCostTracker(options: SessionCostOptions): SessionCostTracker;
|
|
68
70
|
/** The same measurement over a list, for a caller holding one already. */
|
|
69
71
|
export declare function sessionCostShapes(records: readonly UsageRecord[], options: SessionCostOptions): SessionCostShape[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-cost.d.ts","sourceRoot":"","sources":["../src/session-cost.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,MAAM,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,gBAAgB,CAAC;IAC5B,EAAE,CAAC,EAAE,IAAI,CAAC;IACV;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/B,MAAM,IAAI,gBAAgB,EAAE,CAAC;CAC9B;
|
|
1
|
+
{"version":3,"file":"session-cost.d.ts","sourceRoot":"","sources":["../src/session-cost.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,MAAM,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,gBAAgB,CAAC;IAC5B,EAAE,CAAC,EAAE,IAAI,CAAC;IACV;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/B,MAAM,IAAI,gBAAgB,EAAE,CAAC;CAC9B;AAED,iEAAiE;AACjE,wBAAgB,MAAM,CAAC,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,gBAAgB,EAAE,EAAE,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAahG;AAgBD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,kBAAkB,GAAG,kBAAkB,CAwDxF;AAED,0EAA0E;AAC1E,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,SAAS,WAAW,EAAE,EAC/B,OAAO,EAAE,kBAAkB,GAC1B,gBAAgB,EAAE,CAIpB"}
|
package/dist/session-cost.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { effectivePricing, multipliersFor } from './pricing.js';
|
|
2
2
|
import { UNLABELLED } from './usage.js';
|
|
3
3
|
/** Every billed dollar of one call, at its own model's rates. */
|
|
4
|
-
function costOf(record, catalogue, on) {
|
|
4
|
+
export function costOf(record, catalogue, on) {
|
|
5
5
|
const model = catalogue.byId.get(record.model);
|
|
6
6
|
if (!model)
|
|
7
7
|
return null;
|
package/dist/session-cost.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-cost.js","sourceRoot":"","sources":["../src/session-cost.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAwExC,iEAAiE;AACjE,
|
|
1
|
+
{"version":3,"file":"session-cost.js","sourceRoot":"","sources":["../src/session-cost.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trazum/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.66.0",
|
|
4
4
|
"description": "Trazum core: priced advisories for LLM prompts (caching, model tier, batching, schemas), plus deterministic trimming, token counting and pricing.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "David Mu\u00f1oz Rey",
|
package/src/config-schema.ts
CHANGED
|
@@ -137,6 +137,34 @@ export interface SpendConfig {
|
|
|
137
137
|
substitute?: Record<string, { to: string; reason: string }>;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* The enforcement policy: USD ceilings judged at call time, at whichever door
|
|
142
|
+
* the call arrives — the gateway's 402, `serve`'s cost answer, `spend_guard`
|
|
143
|
+
* over MCP.
|
|
144
|
+
*
|
|
145
|
+
* A separate block from `spend`, and the separation is the point. `spend`
|
|
146
|
+
* gates *reports*: it is read after the fact, over a log or a store, and a
|
|
147
|
+
* crossing fails a check. This is read *before a call is made*, to refuse it.
|
|
148
|
+
* Same units, different moments — and the 1.62 arc's lesson is that two
|
|
149
|
+
* surfaces reading "the same" value from different slices of config is how
|
|
150
|
+
* they come to disagree about how much is left. One block, stated once,
|
|
151
|
+
* enforced identically at every door.
|
|
152
|
+
*
|
|
153
|
+
* Every ceiling is a **positive** dollar amount. `spend` accepts zero because
|
|
154
|
+
* a zero report-budget is a gate somebody can waive on the record; a zero
|
|
155
|
+
* enforcement ceiling is a door that refuses everything, which is an outage
|
|
156
|
+
* dressed as a policy — if the intent is "stop all calls", turning the door
|
|
157
|
+
* off is the honest spelling.
|
|
158
|
+
*/
|
|
159
|
+
export interface LimitsConfig {
|
|
160
|
+
/** Ceiling per UTC calendar day, in dollars, measured across the store. */
|
|
161
|
+
dayUsd?: number;
|
|
162
|
+
/** Ceiling per session — the unit an agent product actually blows up in. */
|
|
163
|
+
sessionUsd?: number;
|
|
164
|
+
/** Per-label ceilings, each judged against that label's own measured spend. */
|
|
165
|
+
byLabel?: Record<string, number>;
|
|
166
|
+
}
|
|
167
|
+
|
|
140
168
|
export interface TrazumConfig {
|
|
141
169
|
level?: RuleLevel;
|
|
142
170
|
locale?: Locale;
|
|
@@ -182,6 +210,7 @@ export interface TrazumConfig {
|
|
|
182
210
|
*/
|
|
183
211
|
sources?: Record<string, string[]>;
|
|
184
212
|
spend?: SpendConfig;
|
|
213
|
+
limits?: LimitsConfig;
|
|
185
214
|
/**
|
|
186
215
|
* Findings as policy: a gate failure the team has looked at and decided to
|
|
187
216
|
* live with, on the record, for a bounded time.
|
|
@@ -298,6 +327,7 @@ export const CONFIG_KEYS = [
|
|
|
298
327
|
'budgets',
|
|
299
328
|
'labels',
|
|
300
329
|
'spend',
|
|
330
|
+
'limits',
|
|
301
331
|
'sources',
|
|
302
332
|
'store',
|
|
303
333
|
'waive',
|
|
@@ -315,6 +345,8 @@ export const CONFIG_BASELINE_KEYS = ['path', 'maxGrowthTokens', 'maxGrowthPct']
|
|
|
315
345
|
|
|
316
346
|
export const CONFIG_SPEND_KEYS = ['maxUsd', 'monthlyUsd', 'maxDayUsd', 'maxSessionUsd', 'maxCacheLossUsd', 'byLabel', 'bySource', 'substitute'] as const;
|
|
317
347
|
|
|
348
|
+
export const CONFIG_LIMITS_KEYS = ['dayUsd', 'sessionUsd', 'byLabel'] as const;
|
|
349
|
+
|
|
318
350
|
export const CONFIG_WAIVE_KEYS = ['gate', 'reason', 'until'] as const;
|
|
319
351
|
|
|
320
352
|
export const CONFIG_STORE_KEYS = ['keepDays'] as const;
|
|
@@ -590,6 +622,53 @@ function parseSpend(raw: unknown, source: string): SpendConfig {
|
|
|
590
622
|
return spend;
|
|
591
623
|
}
|
|
592
624
|
|
|
625
|
+
/**
|
|
626
|
+
* Validates the `limits` block.
|
|
627
|
+
*
|
|
628
|
+
* Every ceiling must be a **positive** finite number of dollars — stricter
|
|
629
|
+
* than `spend`, deliberately. A report-budget of zero is a gate somebody can
|
|
630
|
+
* waive on the record; an enforcement ceiling of zero refuses every call at
|
|
631
|
+
* that door, which is an outage dressed as a policy, so the error says what
|
|
632
|
+
* to write instead. Unknown keys are named rather than ignored, because a
|
|
633
|
+
* misspelled ceiling is a policy that is never enforced while CI stays green.
|
|
634
|
+
*/
|
|
635
|
+
function requirePositiveUsd(value: unknown, label: string, source: string): number {
|
|
636
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
637
|
+
throw new ConfigError(
|
|
638
|
+
`"${label}" must be a positive number of dollars. A ceiling of 0 would refuse every call at this door — if that is the intent, turn the door off rather than writing an outage as a policy.`,
|
|
639
|
+
source,
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
return value;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
export function parseLimits(raw: unknown, source: string): LimitsConfig {
|
|
646
|
+
if (!isPlainObject(raw)) throw new ConfigError('"limits" must be an object', source);
|
|
647
|
+
rejectUnknownKeys(raw, CONFIG_LIMITS_KEYS, source, 'limits.');
|
|
648
|
+
|
|
649
|
+
const limits: LimitsConfig = {};
|
|
650
|
+
if (raw.dayUsd !== undefined) {
|
|
651
|
+
limits.dayUsd = requirePositiveUsd(raw.dayUsd, 'limits.dayUsd', source);
|
|
652
|
+
}
|
|
653
|
+
if (raw.sessionUsd !== undefined) {
|
|
654
|
+
limits.sessionUsd = requirePositiveUsd(raw.sessionUsd, 'limits.sessionUsd', source);
|
|
655
|
+
}
|
|
656
|
+
if (raw.byLabel !== undefined) {
|
|
657
|
+
if (!isPlainObject(raw.byLabel)) {
|
|
658
|
+
throw new ConfigError('"limits.byLabel" must be an object', source);
|
|
659
|
+
}
|
|
660
|
+
const byLabel: Record<string, number> = {};
|
|
661
|
+
for (const [label, value] of Object.entries(raw.byLabel)) {
|
|
662
|
+
if (label.trim().length === 0) {
|
|
663
|
+
throw new ConfigError('"limits.byLabel" has an empty label', source);
|
|
664
|
+
}
|
|
665
|
+
byLabel[label] = requirePositiveUsd(value, `limits.byLabel["${label}"]`, source);
|
|
666
|
+
}
|
|
667
|
+
limits.byLabel = byLabel;
|
|
668
|
+
}
|
|
669
|
+
return limits;
|
|
670
|
+
}
|
|
671
|
+
|
|
593
672
|
/**
|
|
594
673
|
* Validates the `sources` block: named services, each a non-empty list of
|
|
595
674
|
* glob patterns. Patterns are not checked against a filesystem here — a
|
|
@@ -869,7 +948,7 @@ function parseStore(raw: unknown, source: string): { keepDays?: number } {
|
|
|
869
948
|
return out;
|
|
870
949
|
}
|
|
871
950
|
|
|
872
|
-
function parseWaive(raw: unknown, source: string): WaiveEntry[] {
|
|
951
|
+
export function parseWaive(raw: unknown, source: string): WaiveEntry[] {
|
|
873
952
|
if (!Array.isArray(raw)) throw new ConfigError('"waive" must be an array', source);
|
|
874
953
|
return raw.map((entry, index) => {
|
|
875
954
|
const at = `waive[${index}]`;
|
|
@@ -882,10 +961,16 @@ function parseWaive(raw: unknown, source: string): WaiveEntry[] {
|
|
|
882
961
|
}
|
|
883
962
|
const known =
|
|
884
963
|
(WAIVABLE_GATES as readonly string[]).includes(gate) ||
|
|
885
|
-
(gate.startsWith('byLabel:') && gate.length > 'byLabel:'.length)
|
|
964
|
+
(gate.startsWith('byLabel:') && gate.length > 'byLabel:'.length) ||
|
|
965
|
+
// The limits policy's ceilings, waivable since the 1.66 arc: silencing
|
|
966
|
+
// a limit leaves a record, and the doors carry the waiver in every
|
|
967
|
+
// judgement rather than refusing. The forms mirror the config keys.
|
|
968
|
+
gate === 'limits.dayUsd' ||
|
|
969
|
+
gate === 'limits.sessionUsd' ||
|
|
970
|
+
(gate.startsWith('limits.byLabel:') && gate.length > 'limits.byLabel:'.length);
|
|
886
971
|
if (!known) {
|
|
887
972
|
throw new ConfigError(
|
|
888
|
-
`"${at}.gate" names no gate: "${gate}". Waivable: ${WAIVABLE_GATES.join(', ')}, or byLabel:<label>.`,
|
|
973
|
+
`"${at}.gate" names no gate: "${gate}". Waivable: ${WAIVABLE_GATES.join(', ')}, byLabel:<label>, limits.dayUsd, limits.sessionUsd, or limits.byLabel:<label>.`,
|
|
889
974
|
source,
|
|
890
975
|
);
|
|
891
976
|
}
|
|
@@ -1056,6 +1141,7 @@ export function parseConfig(raw: string, source = CONFIG_FILENAME): TrazumConfig
|
|
|
1056
1141
|
if (document.budgets !== undefined) config.budgets = parseBudgets(document.budgets, source);
|
|
1057
1142
|
if (document.labels !== undefined) config.labels = parseLabels(document.labels, source);
|
|
1058
1143
|
if (document.spend !== undefined) config.spend = parseSpend(document.spend, source);
|
|
1144
|
+
if (document.limits !== undefined) config.limits = parseLimits(document.limits, source);
|
|
1059
1145
|
if (document.sources !== undefined) config.sources = parseSources(document.sources, source);
|
|
1060
1146
|
if (document.store !== undefined) config.store = parseStore(document.store, source);
|
|
1061
1147
|
if (document.waive !== undefined) config.waive = parseWaive(document.waive, source);
|
package/src/gateway.ts
CHANGED
|
@@ -51,6 +51,9 @@ import type { PlanAssumption } from './plan.js';
|
|
|
51
51
|
import { effectivePricing, multipliersFor } from './pricing.js';
|
|
52
52
|
import type { PricingCatalogue } from './pricing.js';
|
|
53
53
|
import type { ModelPricing } from './types.js';
|
|
54
|
+
import { judgeLimits, limitSentence, unjudgedSentence } from './judgement.js';
|
|
55
|
+
import type { MeasuredPosition, PolicyJudgement } from './judgement.js';
|
|
56
|
+
import type { LimitsConfig, WaiveEntry } from './config-schema.js';
|
|
54
57
|
|
|
55
58
|
/**
|
|
56
59
|
* What the operator has decided happens when the gateway cannot judge.
|
|
@@ -80,6 +83,12 @@ export interface GatewayCall {
|
|
|
80
83
|
maxOutputTokens: number | null;
|
|
81
84
|
/** The workload, when the caller labelled it. */
|
|
82
85
|
label: string | null;
|
|
86
|
+
/**
|
|
87
|
+
* The conversation, when the caller identified it — `metadata.trazum_session`,
|
|
88
|
+
* the same seam as the label. Used to judge the per-session ceiling and
|
|
89
|
+
* never recorded, printed, or forwarded anywhere by this module.
|
|
90
|
+
*/
|
|
91
|
+
session: string | null;
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
/** Where the budget stands, as the gateway was told at the last refresh. */
|
|
@@ -97,10 +106,17 @@ export type RefuseReason =
|
|
|
97
106
|
| 'budget-exhausted'
|
|
98
107
|
/** This call would take it past, on an estimate of this call. */
|
|
99
108
|
| 'call-would-cross'
|
|
109
|
+
/** A ceiling in the `limits` policy is over — the judgement names which. */
|
|
110
|
+
| 'limit-over'
|
|
100
111
|
/** Cannot tell, and the operator chose fail-closed. */
|
|
101
112
|
| 'cannot-tell-and-closed';
|
|
102
113
|
|
|
103
|
-
export type CannotTellCause =
|
|
114
|
+
export type CannotTellCause =
|
|
115
|
+
| 'no-budget'
|
|
116
|
+
| 'nothing-measured'
|
|
117
|
+
| 'model-unpriced'
|
|
118
|
+
/** A ceiling in the `limits` policy could not be judged — see `policy`. */
|
|
119
|
+
| 'limit-unjudged';
|
|
104
120
|
|
|
105
121
|
/** A cheaper way to make the same call, named on a refusal. */
|
|
106
122
|
export interface GatewayAlternative {
|
|
@@ -124,6 +140,8 @@ export type GatewayDecision =
|
|
|
124
140
|
estimatedUsd: number | null;
|
|
125
141
|
/** Present when the gateway could not judge and the operator fails open. */
|
|
126
142
|
unjudged: CannotTellCause | null;
|
|
143
|
+
/** The limits policy, judged for this call — same judge as every door. */
|
|
144
|
+
policy: PolicyJudgement;
|
|
127
145
|
}
|
|
128
146
|
| {
|
|
129
147
|
kind: 'refuse';
|
|
@@ -137,6 +155,8 @@ export type GatewayDecision =
|
|
|
137
155
|
/** A refusal never arrives bare. Dearest saving first; may be empty. */
|
|
138
156
|
alternatives: GatewayAlternative[];
|
|
139
157
|
because: string;
|
|
158
|
+
/** The limits policy, judged for this call — same judge as every door. */
|
|
159
|
+
policy: PolicyJudgement;
|
|
140
160
|
}
|
|
141
161
|
| {
|
|
142
162
|
/**
|
|
@@ -154,6 +174,8 @@ export type GatewayDecision =
|
|
|
154
174
|
configuredReason: string;
|
|
155
175
|
estimatedUsd: number | null;
|
|
156
176
|
markedInStore: true;
|
|
177
|
+
/** The limits policy, judged for this call — same judge as every door. */
|
|
178
|
+
policy: PolicyJudgement;
|
|
157
179
|
};
|
|
158
180
|
|
|
159
181
|
export interface GatewayPolicy {
|
|
@@ -172,6 +194,19 @@ export interface GatewayOptions {
|
|
|
172
194
|
catalogue: PricingCatalogue;
|
|
173
195
|
policy: GatewayPolicy;
|
|
174
196
|
on?: Date;
|
|
197
|
+
/**
|
|
198
|
+
* The `limits` block and the measured position it is judged against —
|
|
199
|
+
* chapter three of the 1.66 arc. Both optional: a gateway with no limits
|
|
200
|
+
* config behaves exactly as before, and the judgement is `no-policy`.
|
|
201
|
+
*
|
|
202
|
+
* The judging is `judgeLimits`, verbatim — the same function `serve` and
|
|
203
|
+
* `spend_guard` call, which is the whole point: three doors, one judge,
|
|
204
|
+
* no door doing its own arithmetic.
|
|
205
|
+
*/
|
|
206
|
+
limits?: LimitsConfig;
|
|
207
|
+
position?: MeasuredPosition;
|
|
208
|
+
/** The config's `waive` list — a silenced limit forwards, on the record. */
|
|
209
|
+
waivers?: readonly WaiveEntry[];
|
|
175
210
|
}
|
|
176
211
|
|
|
177
212
|
/** What this call costs at a model's rates, or null when it cannot be priced. */
|
|
@@ -258,6 +293,63 @@ export function gatewayDecision(
|
|
|
258
293
|
const model = catalogue.byId.get(call.model);
|
|
259
294
|
const estimatedUsd = priceCall(model, call, on);
|
|
260
295
|
|
|
296
|
+
/**
|
|
297
|
+
* The limits policy, judged first and by the shared judge — never by
|
|
298
|
+
* arithmetic of this module's own. An `over` here refuses regardless of
|
|
299
|
+
* where the monthly budget stands: a session ceiling exists precisely for
|
|
300
|
+
* the call that is fine by the month and ruinous by the conversation.
|
|
301
|
+
*/
|
|
302
|
+
const judged = judgeLimits(
|
|
303
|
+
options.limits,
|
|
304
|
+
options.position ?? { dayUsd: null, sessionUsd: null, labelUsd: null },
|
|
305
|
+
{
|
|
306
|
+
model: call.model,
|
|
307
|
+
inputTokens: call.inputTokens ?? undefined,
|
|
308
|
+
outputTokens: call.maxOutputTokens ?? undefined,
|
|
309
|
+
label: call.label ?? undefined,
|
|
310
|
+
session: call.session ?? undefined,
|
|
311
|
+
},
|
|
312
|
+
{ catalogue, on, ...(options.waivers === undefined ? {} : { waivers: options.waivers }) },
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
if (judged.verdict === 'over') {
|
|
316
|
+
// The unwaived crossing — a waived `over` is present in the judgement
|
|
317
|
+
// and deliberately not the one a refusal is written about.
|
|
318
|
+
const over = judged.judgements.find((entry) => entry.verdict === 'over' && entry.waived === null)!;
|
|
319
|
+
/*
|
|
320
|
+
A configured substitution is deliberately NOT consulted here. The
|
|
321
|
+
operator wrote that rule for the budget; rerouting a call because a
|
|
322
|
+
*session* ceiling crossed would spend more money in the very unit that
|
|
323
|
+
is over, under a rule written about something else.
|
|
324
|
+
*/
|
|
325
|
+
return {
|
|
326
|
+
kind: 'refuse',
|
|
327
|
+
reason: 'limit-over',
|
|
328
|
+
cause: null,
|
|
329
|
+
restsOn: over.restsOn,
|
|
330
|
+
standing,
|
|
331
|
+
estimatedUsd,
|
|
332
|
+
alternatives: model === undefined ? [] : alternativesFor(model, call, catalogue, on),
|
|
333
|
+
because: limitSentence(over),
|
|
334
|
+
policy: judged,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const limitUnjudged = judged.verdict === 'cannot-tell' && judged.reason !== 'no-policy';
|
|
339
|
+
if (limitUnjudged && policy.onCannotTell === 'fail-closed') {
|
|
340
|
+
return {
|
|
341
|
+
kind: 'refuse',
|
|
342
|
+
reason: 'cannot-tell-and-closed',
|
|
343
|
+
cause: 'limit-unjudged',
|
|
344
|
+
restsOn: null,
|
|
345
|
+
standing,
|
|
346
|
+
estimatedUsd,
|
|
347
|
+
alternatives: [],
|
|
348
|
+
because: `${unjudgedSentence(judged)} This gateway is configured to fail closed.`,
|
|
349
|
+
policy: judged,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
261
353
|
const cause = whyCannotTell(standing, estimatedUsd);
|
|
262
354
|
if (cause !== null) {
|
|
263
355
|
/**
|
|
@@ -267,7 +359,7 @@ export function gatewayDecision(
|
|
|
267
359
|
* nothing to do with the caller's request.
|
|
268
360
|
*/
|
|
269
361
|
if (policy.onCannotTell === 'fail-open') {
|
|
270
|
-
return { kind: 'forward', estimatedUsd, unjudged: cause };
|
|
362
|
+
return { kind: 'forward', estimatedUsd, unjudged: cause, policy: judged };
|
|
271
363
|
}
|
|
272
364
|
return {
|
|
273
365
|
kind: 'refuse',
|
|
@@ -278,6 +370,7 @@ export function gatewayDecision(
|
|
|
278
370
|
estimatedUsd,
|
|
279
371
|
alternatives: [],
|
|
280
372
|
because: becauseCannotTell(cause),
|
|
373
|
+
policy: judged,
|
|
281
374
|
};
|
|
282
375
|
}
|
|
283
376
|
|
|
@@ -285,13 +378,21 @@ export function gatewayDecision(
|
|
|
285
378
|
// it — narrowed here rather than asserted, so a change to `whyCannotTell`
|
|
286
379
|
// that stopped guaranteeing them fails the build instead of the request.
|
|
287
380
|
if (standing === null || estimatedUsd === null || model === undefined) {
|
|
288
|
-
return {
|
|
381
|
+
return {
|
|
382
|
+
kind: 'forward',
|
|
383
|
+
estimatedUsd,
|
|
384
|
+
unjudged: limitUnjudged ? 'limit-unjudged' : 'no-budget',
|
|
385
|
+
policy: judged,
|
|
386
|
+
};
|
|
289
387
|
}
|
|
290
388
|
|
|
291
389
|
const already = standing.consumedUsd > standing.limitUsd;
|
|
292
390
|
const wouldCross = standing.consumedUsd + estimatedUsd > standing.limitUsd;
|
|
293
391
|
if (!already && !wouldCross) {
|
|
294
|
-
|
|
392
|
+
// The monthly budget holds. A limits ceiling nobody could judge still
|
|
393
|
+
// rides along as `unjudged` under fail-open, so the forward says what it
|
|
394
|
+
// did not check rather than implying everything was.
|
|
395
|
+
return { kind: 'forward', estimatedUsd, unjudged: limitUnjudged ? 'limit-unjudged' : null, policy: judged };
|
|
295
396
|
}
|
|
296
397
|
|
|
297
398
|
const configured = policy.substitute?.[call.model];
|
|
@@ -303,6 +404,7 @@ export function gatewayDecision(
|
|
|
303
404
|
configuredReason: configured.reason,
|
|
304
405
|
estimatedUsd: priceCall(target, call, on),
|
|
305
406
|
markedInStore: true,
|
|
407
|
+
policy: judged,
|
|
306
408
|
};
|
|
307
409
|
}
|
|
308
410
|
|
|
@@ -319,9 +421,11 @@ export function gatewayDecision(
|
|
|
319
421
|
because: already
|
|
320
422
|
? 'The budget for this period is already spent, measured.'
|
|
321
423
|
: 'This call would take the budget past its limit, on an estimate of this call.',
|
|
424
|
+
policy: judged,
|
|
322
425
|
};
|
|
323
426
|
}
|
|
324
427
|
|
|
428
|
+
|
|
325
429
|
function becauseCannotTell(cause: CannotTellCause): string {
|
|
326
430
|
return cause === 'no-budget'
|
|
327
431
|
? 'No budget is configured, so there is nothing to judge this against, and this gateway is configured to fail closed.'
|