floe-guard 0.4.0 → 0.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/README.md +19 -3
- package/dist/index.cjs +125 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +89 -14
- package/dist/index.d.ts +89 -14
- package/dist/index.js +125 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -68,7 +68,8 @@ declare namespace pricing {
|
|
|
68
68
|
* One priced spend event in the guard's per-call ledger.
|
|
69
69
|
*
|
|
70
70
|
* Every {@link BudgetGuard.record} / {@link BudgetGuard.settle} /
|
|
71
|
-
* {@link BudgetGuard.recordTool}
|
|
71
|
+
* {@link BudgetGuard.recordTool} / {@link BudgetGuard.settleTool} that accrues
|
|
72
|
+
* spend appends exactly one event, so
|
|
72
73
|
* the ledger's costs sum to `spentUsd` (unless a `maxLogEvents` ring buffer has
|
|
73
74
|
* evicted old events). The schema is identical in the Python
|
|
74
75
|
* package (`SpendEvent` in `src/floe_guard/guard.py`) and
|
|
@@ -149,13 +150,26 @@ declare class BudgetGuard {
|
|
|
149
150
|
failClosed: boolean;
|
|
150
151
|
nearLimitBps: number;
|
|
151
152
|
private readonly onBlock;
|
|
152
|
-
/**
|
|
153
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Costs of the most recent priced LLM call and tool call, tracked
|
|
155
|
+
* SEPARATELY: the default next-call prediction is the max of the two, so a
|
|
156
|
+
* cheap tool call can't shrink the estimate right before an expensive LLM
|
|
157
|
+
* call (or vice versa) — conservative beats one-call-too-late.
|
|
158
|
+
*/
|
|
159
|
+
private lastLlmCost;
|
|
160
|
+
private lastToolCost;
|
|
154
161
|
/** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */
|
|
155
162
|
private reserved;
|
|
156
163
|
/** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */
|
|
157
164
|
private readonly spendEvents;
|
|
158
165
|
private readonly maxLogEvents?;
|
|
166
|
+
/**
|
|
167
|
+
* Per-tool running totals (settleTool/recordTool) — the tool side of the one
|
|
168
|
+
* shared ceiling, exposed via the toolCosts getter. null-prototype: tool
|
|
169
|
+
* names are caller-supplied strings, so a "__proto__" name is stored as
|
|
170
|
+
* plain data instead of mutating the object's prototype.
|
|
171
|
+
*/
|
|
172
|
+
private readonly toolCostTotals;
|
|
159
173
|
/**
|
|
160
174
|
* @param limitUsd the spend ceiling, in USD. `0` blocks the very first call.
|
|
161
175
|
*/
|
|
@@ -164,7 +178,8 @@ declare class BudgetGuard {
|
|
|
164
178
|
* Throw {@link BudgetExceeded} if the next call would cross the ceiling.
|
|
165
179
|
*
|
|
166
180
|
* Call this immediately before each LLM request. The "next call" is estimated
|
|
167
|
-
*
|
|
181
|
+
* conservatively as the costlier of the last LLM call and the last tool call
|
|
182
|
+
* (override with `estimatedNextCost`); the
|
|
168
183
|
* first call is always allowed unless the ceiling is already met. In-flight
|
|
169
184
|
* reservations count toward the total, so this stays correct alongside
|
|
170
185
|
* {@link BudgetGuard.reserve}.
|
|
@@ -181,7 +196,8 @@ declare class BudgetGuard {
|
|
|
181
196
|
* the same stale total. Throws {@link BudgetExceeded} (without reserving) if
|
|
182
197
|
* the reservation would cross the ceiling. Returns the reservation handle to
|
|
183
198
|
* pass to {@link BudgetGuard.settle} (or {@link BudgetGuard.release} on error).
|
|
184
|
-
* `estimatedCost` defaults to the last call
|
|
199
|
+
* `estimatedCost` defaults to the costlier of the last LLM call and the last
|
|
200
|
+
* tool call.
|
|
185
201
|
*/
|
|
186
202
|
reserve(estimatedCost?: number): number;
|
|
187
203
|
/**
|
|
@@ -209,16 +225,49 @@ declare class BudgetGuard {
|
|
|
209
225
|
price?: ManualPrice;
|
|
210
226
|
label?: string;
|
|
211
227
|
}): number;
|
|
228
|
+
/**
|
|
229
|
+
* Atomically check the ceiling AND hold a tool call's cost in flight.
|
|
230
|
+
*
|
|
231
|
+
* The tool-spend counterpart of {@link BudgetGuard.reserve} — and STRONGER
|
|
232
|
+
* than the LLM path, because a paid tool's price is usually known exactly
|
|
233
|
+
* before the call, so the pre-call hard-stop is precise rather than
|
|
234
|
+
* estimated:
|
|
235
|
+
*
|
|
236
|
+
* const handle = guard.reserveTool(0.02); // throws BEFORE Apollo runs
|
|
237
|
+
* const result = await apollo.peopleLookup(...);
|
|
238
|
+
* guard.settleTool("apollo.people_lookup", 0.02, { reserved: handle });
|
|
239
|
+
*
|
|
240
|
+
* Throws {@link BudgetExceeded} (without reserving) if the call would cross
|
|
241
|
+
* the ceiling. The estimate is required — tools have no last-cost prediction
|
|
242
|
+
* worth falling back to. Pass the returned handle to
|
|
243
|
+
* {@link BudgetGuard.settleTool}, or {@link BudgetGuard.release} on failure.
|
|
244
|
+
*/
|
|
245
|
+
reserveTool(estimatedCost: number): number;
|
|
246
|
+
/**
|
|
247
|
+
* Release a reservation and record a tool call's actual cost.
|
|
248
|
+
*
|
|
249
|
+
* `recordTool` is `settleTool` with no reservation. The caller supplies the
|
|
250
|
+
* cost — tools have no token usage to price. Accrues into the same
|
|
251
|
+
* `spentUsd` ceiling as tokens, tallies the per-tool total
|
|
252
|
+
* ({@link BudgetGuard.toolCosts}), updates the tool side of the next-call
|
|
253
|
+
* estimate (tracked separately from the LLM side; the default prediction is
|
|
254
|
+
* the max of the two, so a tool-hammering loop's plain `check()` stops
|
|
255
|
+
* BEFORE the crossing call without a cheap tool shrinking the LLM
|
|
256
|
+
* prediction), and appends
|
|
257
|
+
* a `kind: "tool"` {@link SpendEvent} to {@link BudgetGuard.spendLog}.
|
|
258
|
+
* Returns `costUsd`.
|
|
259
|
+
*/
|
|
260
|
+
settleTool(tool: string, costUsd: number, options?: {
|
|
261
|
+
reserved?: number;
|
|
262
|
+
label?: string;
|
|
263
|
+
}): number;
|
|
212
264
|
/**
|
|
213
265
|
* Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.
|
|
214
266
|
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* cost: tools have no token usage to price. Deliberately does NOT update the
|
|
220
|
-
* next-call estimate — that predicts the next *LLM* call, and a tool's price
|
|
221
|
-
* would skew it. Returns `costUsd`.
|
|
267
|
+
* Post-hoc accrual for costs only known after the call (metered APIs); when
|
|
268
|
+
* the price is known up front, {@link BudgetGuard.reserveTool} /
|
|
269
|
+
* {@link BudgetGuard.settleTool} give the stronger pre-call hard-stop. See
|
|
270
|
+
* `settleTool` for the full contract. Returns `costUsd`.
|
|
222
271
|
*/
|
|
223
272
|
recordTool(tool: string, costUsd: number, options?: {
|
|
224
273
|
label?: string;
|
|
@@ -230,10 +279,17 @@ declare class BudgetGuard {
|
|
|
230
279
|
release(reserved: number): void;
|
|
231
280
|
/** USD left before the ceiling, net of in-flight reservations (never negative). */
|
|
232
281
|
get remainingUsd(): number;
|
|
282
|
+
/**
|
|
283
|
+
* Per-tool running USD totals, keyed by the name given to `settleTool()` /
|
|
284
|
+
* `recordTool()` — e.g. `{"apollo.people_lookup": 0.42, "exa.search": 0.11}`.
|
|
285
|
+
* Makes the token/tool split of the one shared ceiling inspectable
|
|
286
|
+
* (`spentUsd - sum of toolCosts` is the token side). Returns a snapshot copy.
|
|
287
|
+
*/
|
|
288
|
+
get toolCosts(): Record<string, number>;
|
|
233
289
|
/**
|
|
234
290
|
* The per-call spend ledger, oldest first — one {@link SpendEvent} per priced
|
|
235
|
-
* `record()` / `settle()` / `recordTool()`. Returns a
|
|
236
|
-
* it cannot corrupt the ledger.
|
|
291
|
+
* `record()` / `settle()` / `recordTool()` / `settleTool()`. Returns a
|
|
292
|
+
* snapshot copy: mutating it cannot corrupt the ledger.
|
|
237
293
|
*/
|
|
238
294
|
get spendLog(): SpendEvent[];
|
|
239
295
|
/**
|
|
@@ -247,6 +303,25 @@ declare class BudgetGuard {
|
|
|
247
303
|
* Python `2.5e-06`.) Empty ledger yields `""`.
|
|
248
304
|
*/
|
|
249
305
|
exportLog(): string;
|
|
306
|
+
/**
|
|
307
|
+
* The default next-call prediction when the caller supplies no estimate.
|
|
308
|
+
* Conservative: the costlier of the last LLM call and the last tool call — a
|
|
309
|
+
* mixed loop predicts the pricier kind, which at worst blocks one call early
|
|
310
|
+
* (fail-closed) rather than letting a crossing call through because the LAST
|
|
311
|
+
* event happened to be cheap.
|
|
312
|
+
*/
|
|
313
|
+
private defaultEstimate;
|
|
314
|
+
/**
|
|
315
|
+
* Subtract a settled/released hold from the in-flight tally. A handle larger
|
|
316
|
+
* than EVERYTHING currently held cannot have come from a matching
|
|
317
|
+
* `reserve()` — throwing beats silently clamping, which would free OTHER
|
|
318
|
+
* callers' holds and fail the ceiling open. The epsilon absorbs float dust
|
|
319
|
+
* from accumulating and draining many holds; per-caller over-release (a
|
|
320
|
+
* handle within the total but larger than the caller's own hold) is
|
|
321
|
+
* undetectable without per-handle tracking and remains the caller's
|
|
322
|
+
* responsibility.
|
|
323
|
+
*/
|
|
324
|
+
private consumeReservation;
|
|
250
325
|
private appendEvent;
|
|
251
326
|
/**
|
|
252
327
|
* Context-aware spend advisory for this budget — see {@link BudgetAdvisory}.
|
package/dist/index.d.ts
CHANGED
|
@@ -68,7 +68,8 @@ declare namespace pricing {
|
|
|
68
68
|
* One priced spend event in the guard's per-call ledger.
|
|
69
69
|
*
|
|
70
70
|
* Every {@link BudgetGuard.record} / {@link BudgetGuard.settle} /
|
|
71
|
-
* {@link BudgetGuard.recordTool}
|
|
71
|
+
* {@link BudgetGuard.recordTool} / {@link BudgetGuard.settleTool} that accrues
|
|
72
|
+
* spend appends exactly one event, so
|
|
72
73
|
* the ledger's costs sum to `spentUsd` (unless a `maxLogEvents` ring buffer has
|
|
73
74
|
* evicted old events). The schema is identical in the Python
|
|
74
75
|
* package (`SpendEvent` in `src/floe_guard/guard.py`) and
|
|
@@ -149,13 +150,26 @@ declare class BudgetGuard {
|
|
|
149
150
|
failClosed: boolean;
|
|
150
151
|
nearLimitBps: number;
|
|
151
152
|
private readonly onBlock;
|
|
152
|
-
/**
|
|
153
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Costs of the most recent priced LLM call and tool call, tracked
|
|
155
|
+
* SEPARATELY: the default next-call prediction is the max of the two, so a
|
|
156
|
+
* cheap tool call can't shrink the estimate right before an expensive LLM
|
|
157
|
+
* call (or vice versa) — conservative beats one-call-too-late.
|
|
158
|
+
*/
|
|
159
|
+
private lastLlmCost;
|
|
160
|
+
private lastToolCost;
|
|
154
161
|
/** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */
|
|
155
162
|
private reserved;
|
|
156
163
|
/** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */
|
|
157
164
|
private readonly spendEvents;
|
|
158
165
|
private readonly maxLogEvents?;
|
|
166
|
+
/**
|
|
167
|
+
* Per-tool running totals (settleTool/recordTool) — the tool side of the one
|
|
168
|
+
* shared ceiling, exposed via the toolCosts getter. null-prototype: tool
|
|
169
|
+
* names are caller-supplied strings, so a "__proto__" name is stored as
|
|
170
|
+
* plain data instead of mutating the object's prototype.
|
|
171
|
+
*/
|
|
172
|
+
private readonly toolCostTotals;
|
|
159
173
|
/**
|
|
160
174
|
* @param limitUsd the spend ceiling, in USD. `0` blocks the very first call.
|
|
161
175
|
*/
|
|
@@ -164,7 +178,8 @@ declare class BudgetGuard {
|
|
|
164
178
|
* Throw {@link BudgetExceeded} if the next call would cross the ceiling.
|
|
165
179
|
*
|
|
166
180
|
* Call this immediately before each LLM request. The "next call" is estimated
|
|
167
|
-
*
|
|
181
|
+
* conservatively as the costlier of the last LLM call and the last tool call
|
|
182
|
+
* (override with `estimatedNextCost`); the
|
|
168
183
|
* first call is always allowed unless the ceiling is already met. In-flight
|
|
169
184
|
* reservations count toward the total, so this stays correct alongside
|
|
170
185
|
* {@link BudgetGuard.reserve}.
|
|
@@ -181,7 +196,8 @@ declare class BudgetGuard {
|
|
|
181
196
|
* the same stale total. Throws {@link BudgetExceeded} (without reserving) if
|
|
182
197
|
* the reservation would cross the ceiling. Returns the reservation handle to
|
|
183
198
|
* pass to {@link BudgetGuard.settle} (or {@link BudgetGuard.release} on error).
|
|
184
|
-
* `estimatedCost` defaults to the last call
|
|
199
|
+
* `estimatedCost` defaults to the costlier of the last LLM call and the last
|
|
200
|
+
* tool call.
|
|
185
201
|
*/
|
|
186
202
|
reserve(estimatedCost?: number): number;
|
|
187
203
|
/**
|
|
@@ -209,16 +225,49 @@ declare class BudgetGuard {
|
|
|
209
225
|
price?: ManualPrice;
|
|
210
226
|
label?: string;
|
|
211
227
|
}): number;
|
|
228
|
+
/**
|
|
229
|
+
* Atomically check the ceiling AND hold a tool call's cost in flight.
|
|
230
|
+
*
|
|
231
|
+
* The tool-spend counterpart of {@link BudgetGuard.reserve} — and STRONGER
|
|
232
|
+
* than the LLM path, because a paid tool's price is usually known exactly
|
|
233
|
+
* before the call, so the pre-call hard-stop is precise rather than
|
|
234
|
+
* estimated:
|
|
235
|
+
*
|
|
236
|
+
* const handle = guard.reserveTool(0.02); // throws BEFORE Apollo runs
|
|
237
|
+
* const result = await apollo.peopleLookup(...);
|
|
238
|
+
* guard.settleTool("apollo.people_lookup", 0.02, { reserved: handle });
|
|
239
|
+
*
|
|
240
|
+
* Throws {@link BudgetExceeded} (without reserving) if the call would cross
|
|
241
|
+
* the ceiling. The estimate is required — tools have no last-cost prediction
|
|
242
|
+
* worth falling back to. Pass the returned handle to
|
|
243
|
+
* {@link BudgetGuard.settleTool}, or {@link BudgetGuard.release} on failure.
|
|
244
|
+
*/
|
|
245
|
+
reserveTool(estimatedCost: number): number;
|
|
246
|
+
/**
|
|
247
|
+
* Release a reservation and record a tool call's actual cost.
|
|
248
|
+
*
|
|
249
|
+
* `recordTool` is `settleTool` with no reservation. The caller supplies the
|
|
250
|
+
* cost — tools have no token usage to price. Accrues into the same
|
|
251
|
+
* `spentUsd` ceiling as tokens, tallies the per-tool total
|
|
252
|
+
* ({@link BudgetGuard.toolCosts}), updates the tool side of the next-call
|
|
253
|
+
* estimate (tracked separately from the LLM side; the default prediction is
|
|
254
|
+
* the max of the two, so a tool-hammering loop's plain `check()` stops
|
|
255
|
+
* BEFORE the crossing call without a cheap tool shrinking the LLM
|
|
256
|
+
* prediction), and appends
|
|
257
|
+
* a `kind: "tool"` {@link SpendEvent} to {@link BudgetGuard.spendLog}.
|
|
258
|
+
* Returns `costUsd`.
|
|
259
|
+
*/
|
|
260
|
+
settleTool(tool: string, costUsd: number, options?: {
|
|
261
|
+
reserved?: number;
|
|
262
|
+
label?: string;
|
|
263
|
+
}): number;
|
|
212
264
|
/**
|
|
213
265
|
* Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.
|
|
214
266
|
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* cost: tools have no token usage to price. Deliberately does NOT update the
|
|
220
|
-
* next-call estimate — that predicts the next *LLM* call, and a tool's price
|
|
221
|
-
* would skew it. Returns `costUsd`.
|
|
267
|
+
* Post-hoc accrual for costs only known after the call (metered APIs); when
|
|
268
|
+
* the price is known up front, {@link BudgetGuard.reserveTool} /
|
|
269
|
+
* {@link BudgetGuard.settleTool} give the stronger pre-call hard-stop. See
|
|
270
|
+
* `settleTool` for the full contract. Returns `costUsd`.
|
|
222
271
|
*/
|
|
223
272
|
recordTool(tool: string, costUsd: number, options?: {
|
|
224
273
|
label?: string;
|
|
@@ -230,10 +279,17 @@ declare class BudgetGuard {
|
|
|
230
279
|
release(reserved: number): void;
|
|
231
280
|
/** USD left before the ceiling, net of in-flight reservations (never negative). */
|
|
232
281
|
get remainingUsd(): number;
|
|
282
|
+
/**
|
|
283
|
+
* Per-tool running USD totals, keyed by the name given to `settleTool()` /
|
|
284
|
+
* `recordTool()` — e.g. `{"apollo.people_lookup": 0.42, "exa.search": 0.11}`.
|
|
285
|
+
* Makes the token/tool split of the one shared ceiling inspectable
|
|
286
|
+
* (`spentUsd - sum of toolCosts` is the token side). Returns a snapshot copy.
|
|
287
|
+
*/
|
|
288
|
+
get toolCosts(): Record<string, number>;
|
|
233
289
|
/**
|
|
234
290
|
* The per-call spend ledger, oldest first — one {@link SpendEvent} per priced
|
|
235
|
-
* `record()` / `settle()` / `recordTool()`. Returns a
|
|
236
|
-
* it cannot corrupt the ledger.
|
|
291
|
+
* `record()` / `settle()` / `recordTool()` / `settleTool()`. Returns a
|
|
292
|
+
* snapshot copy: mutating it cannot corrupt the ledger.
|
|
237
293
|
*/
|
|
238
294
|
get spendLog(): SpendEvent[];
|
|
239
295
|
/**
|
|
@@ -247,6 +303,25 @@ declare class BudgetGuard {
|
|
|
247
303
|
* Python `2.5e-06`.) Empty ledger yields `""`.
|
|
248
304
|
*/
|
|
249
305
|
exportLog(): string;
|
|
306
|
+
/**
|
|
307
|
+
* The default next-call prediction when the caller supplies no estimate.
|
|
308
|
+
* Conservative: the costlier of the last LLM call and the last tool call — a
|
|
309
|
+
* mixed loop predicts the pricier kind, which at worst blocks one call early
|
|
310
|
+
* (fail-closed) rather than letting a crossing call through because the LAST
|
|
311
|
+
* event happened to be cheap.
|
|
312
|
+
*/
|
|
313
|
+
private defaultEstimate;
|
|
314
|
+
/**
|
|
315
|
+
* Subtract a settled/released hold from the in-flight tally. A handle larger
|
|
316
|
+
* than EVERYTHING currently held cannot have come from a matching
|
|
317
|
+
* `reserve()` — throwing beats silently clamping, which would free OTHER
|
|
318
|
+
* callers' holds and fail the ceiling open. The epsilon absorbs float dust
|
|
319
|
+
* from accumulating and draining many holds; per-caller over-release (a
|
|
320
|
+
* handle within the total but larger than the caller's own hold) is
|
|
321
|
+
* undetectable without per-handle tracking and remains the caller's
|
|
322
|
+
* responsibility.
|
|
323
|
+
*/
|
|
324
|
+
private consumeReservation;
|
|
250
325
|
private appendEvent;
|
|
251
326
|
/**
|
|
252
327
|
* Context-aware spend advisory for this budget — see {@link BudgetAdvisory}.
|
package/dist/index.js
CHANGED
|
@@ -963,13 +963,26 @@ var BudgetGuard = class {
|
|
|
963
963
|
failClosed;
|
|
964
964
|
nearLimitBps;
|
|
965
965
|
onBlock;
|
|
966
|
-
/**
|
|
967
|
-
|
|
966
|
+
/**
|
|
967
|
+
* Costs of the most recent priced LLM call and tool call, tracked
|
|
968
|
+
* SEPARATELY: the default next-call prediction is the max of the two, so a
|
|
969
|
+
* cheap tool call can't shrink the estimate right before an expensive LLM
|
|
970
|
+
* call (or vice versa) — conservative beats one-call-too-late.
|
|
971
|
+
*/
|
|
972
|
+
lastLlmCost = 0;
|
|
973
|
+
lastToolCost = 0;
|
|
968
974
|
/** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */
|
|
969
975
|
reserved = 0;
|
|
970
976
|
/** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */
|
|
971
977
|
spendEvents = [];
|
|
972
978
|
maxLogEvents;
|
|
979
|
+
/**
|
|
980
|
+
* Per-tool running totals (settleTool/recordTool) — the tool side of the one
|
|
981
|
+
* shared ceiling, exposed via the toolCosts getter. null-prototype: tool
|
|
982
|
+
* names are caller-supplied strings, so a "__proto__" name is stored as
|
|
983
|
+
* plain data instead of mutating the object's prototype.
|
|
984
|
+
*/
|
|
985
|
+
toolCostTotals = /* @__PURE__ */ Object.create(null);
|
|
973
986
|
/**
|
|
974
987
|
* @param limitUsd the spend ceiling, in USD. `0` blocks the very first call.
|
|
975
988
|
*/
|
|
@@ -1001,7 +1014,8 @@ var BudgetGuard = class {
|
|
|
1001
1014
|
* Throw {@link BudgetExceeded} if the next call would cross the ceiling.
|
|
1002
1015
|
*
|
|
1003
1016
|
* Call this immediately before each LLM request. The "next call" is estimated
|
|
1004
|
-
*
|
|
1017
|
+
* conservatively as the costlier of the last LLM call and the last tool call
|
|
1018
|
+
* (override with `estimatedNextCost`); the
|
|
1005
1019
|
* first call is always allowed unless the ceiling is already met. In-flight
|
|
1006
1020
|
* reservations count toward the total, so this stays correct alongside
|
|
1007
1021
|
* {@link BudgetGuard.reserve}.
|
|
@@ -1010,7 +1024,7 @@ var BudgetGuard = class {
|
|
|
1010
1024
|
* `settle()`, which hold the estimate across the await.
|
|
1011
1025
|
*/
|
|
1012
1026
|
check(estimatedNextCost) {
|
|
1013
|
-
const rawEstimate = estimatedNextCost === void 0 ? this.
|
|
1027
|
+
const rawEstimate = estimatedNextCost === void 0 ? this.defaultEstimate() : estimatedNextCost;
|
|
1014
1028
|
if (!Number.isFinite(rawEstimate)) {
|
|
1015
1029
|
throw new RangeError(
|
|
1016
1030
|
`estimatedNextCost must be a finite number, got ${rawEstimate}`
|
|
@@ -1031,10 +1045,11 @@ var BudgetGuard = class {
|
|
|
1031
1045
|
* the same stale total. Throws {@link BudgetExceeded} (without reserving) if
|
|
1032
1046
|
* the reservation would cross the ceiling. Returns the reservation handle to
|
|
1033
1047
|
* pass to {@link BudgetGuard.settle} (or {@link BudgetGuard.release} on error).
|
|
1034
|
-
* `estimatedCost` defaults to the last call
|
|
1048
|
+
* `estimatedCost` defaults to the costlier of the last LLM call and the last
|
|
1049
|
+
* tool call.
|
|
1035
1050
|
*/
|
|
1036
1051
|
reserve(estimatedCost) {
|
|
1037
|
-
const rawEstimate = estimatedCost === void 0 ? this.
|
|
1052
|
+
const rawEstimate = estimatedCost === void 0 ? this.defaultEstimate() : estimatedCost;
|
|
1038
1053
|
if (!Number.isFinite(rawEstimate)) {
|
|
1039
1054
|
throw new RangeError(
|
|
1040
1055
|
`estimatedCost must be a finite number, got ${rawEstimate}`
|
|
@@ -1086,13 +1101,13 @@ var BudgetGuard = class {
|
|
|
1086
1101
|
throw err;
|
|
1087
1102
|
}
|
|
1088
1103
|
if (reserved) {
|
|
1089
|
-
this.
|
|
1104
|
+
this.consumeReservation(reserved);
|
|
1090
1105
|
}
|
|
1091
1106
|
this.spentUsd += cost;
|
|
1092
1107
|
if (this.spentUsd - this.limitUsd > 0 && this.spentUsd - this.limitUsd < EPS) {
|
|
1093
1108
|
this.spentUsd = this.limitUsd;
|
|
1094
1109
|
}
|
|
1095
|
-
this.
|
|
1110
|
+
this.lastLlmCost = cost;
|
|
1096
1111
|
this.appendEvent({
|
|
1097
1112
|
timestamp: Date.now() / 1e3,
|
|
1098
1113
|
kind: "llm",
|
|
@@ -1122,24 +1137,64 @@ var BudgetGuard = class {
|
|
|
1122
1137
|
});
|
|
1123
1138
|
}
|
|
1124
1139
|
/**
|
|
1125
|
-
*
|
|
1140
|
+
* Atomically check the ceiling AND hold a tool call's cost in flight.
|
|
1141
|
+
*
|
|
1142
|
+
* The tool-spend counterpart of {@link BudgetGuard.reserve} — and STRONGER
|
|
1143
|
+
* than the LLM path, because a paid tool's price is usually known exactly
|
|
1144
|
+
* before the call, so the pre-call hard-stop is precise rather than
|
|
1145
|
+
* estimated:
|
|
1126
1146
|
*
|
|
1127
|
-
*
|
|
1128
|
-
*
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1147
|
+
* const handle = guard.reserveTool(0.02); // throws BEFORE Apollo runs
|
|
1148
|
+
* const result = await apollo.peopleLookup(...);
|
|
1149
|
+
* guard.settleTool("apollo.people_lookup", 0.02, { reserved: handle });
|
|
1150
|
+
*
|
|
1151
|
+
* Throws {@link BudgetExceeded} (without reserving) if the call would cross
|
|
1152
|
+
* the ceiling. The estimate is required — tools have no last-cost prediction
|
|
1153
|
+
* worth falling back to. Pass the returned handle to
|
|
1154
|
+
* {@link BudgetGuard.settleTool}, or {@link BudgetGuard.release} on failure.
|
|
1134
1155
|
*/
|
|
1135
|
-
|
|
1156
|
+
reserveTool(estimatedCost) {
|
|
1157
|
+
if (estimatedCost === void 0) {
|
|
1158
|
+
throw new RangeError("reserveTool requires an estimated cost, got undefined");
|
|
1159
|
+
}
|
|
1160
|
+
if (!Number.isFinite(estimatedCost) || estimatedCost < 0) {
|
|
1161
|
+
throw new RangeError(
|
|
1162
|
+
`estimatedCost must be a finite, non-negative number, got ${estimatedCost}`
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
return this.reserve(estimatedCost);
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Release a reservation and record a tool call's actual cost.
|
|
1169
|
+
*
|
|
1170
|
+
* `recordTool` is `settleTool` with no reservation. The caller supplies the
|
|
1171
|
+
* cost — tools have no token usage to price. Accrues into the same
|
|
1172
|
+
* `spentUsd` ceiling as tokens, tallies the per-tool total
|
|
1173
|
+
* ({@link BudgetGuard.toolCosts}), updates the tool side of the next-call
|
|
1174
|
+
* estimate (tracked separately from the LLM side; the default prediction is
|
|
1175
|
+
* the max of the two, so a tool-hammering loop's plain `check()` stops
|
|
1176
|
+
* BEFORE the crossing call without a cheap tool shrinking the LLM
|
|
1177
|
+
* prediction), and appends
|
|
1178
|
+
* a `kind: "tool"` {@link SpendEvent} to {@link BudgetGuard.spendLog}.
|
|
1179
|
+
* Returns `costUsd`.
|
|
1180
|
+
*/
|
|
1181
|
+
settleTool(tool, costUsd, options = {}) {
|
|
1136
1182
|
if (!Number.isFinite(costUsd) || costUsd < 0) {
|
|
1137
1183
|
throw new RangeError(`costUsd must be a finite, non-negative number, got ${costUsd}`);
|
|
1138
1184
|
}
|
|
1185
|
+
const reserved = options.reserved ?? 0;
|
|
1186
|
+
if (!Number.isFinite(reserved) || reserved < 0) {
|
|
1187
|
+
throw new RangeError(`reserved must be a finite, non-negative number, got ${reserved}`);
|
|
1188
|
+
}
|
|
1189
|
+
if (reserved) {
|
|
1190
|
+
this.consumeReservation(reserved);
|
|
1191
|
+
}
|
|
1139
1192
|
this.spentUsd += costUsd;
|
|
1140
1193
|
if (this.spentUsd - this.limitUsd > 0 && this.spentUsd - this.limitUsd < EPS) {
|
|
1141
1194
|
this.spentUsd = this.limitUsd;
|
|
1142
1195
|
}
|
|
1196
|
+
this.lastToolCost = costUsd;
|
|
1197
|
+
this.toolCostTotals[tool] = (this.toolCostTotals[tool] ?? 0) + costUsd;
|
|
1143
1198
|
this.appendEvent({
|
|
1144
1199
|
timestamp: Date.now() / 1e3,
|
|
1145
1200
|
kind: "tool",
|
|
@@ -1147,10 +1202,22 @@ var BudgetGuard = class {
|
|
|
1147
1202
|
promptTokens: null,
|
|
1148
1203
|
completionTokens: null,
|
|
1149
1204
|
costUsd,
|
|
1150
|
-
...options.label !== void 0 ? { label: options.label } : {}
|
|
1205
|
+
...options.label !== void 0 ? { label: options.label } : {},
|
|
1206
|
+
...reserved ? { reserved } : {}
|
|
1151
1207
|
});
|
|
1152
1208
|
return costUsd;
|
|
1153
1209
|
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.
|
|
1212
|
+
*
|
|
1213
|
+
* Post-hoc accrual for costs only known after the call (metered APIs); when
|
|
1214
|
+
* the price is known up front, {@link BudgetGuard.reserveTool} /
|
|
1215
|
+
* {@link BudgetGuard.settleTool} give the stronger pre-call hard-stop. See
|
|
1216
|
+
* `settleTool` for the full contract. Returns `costUsd`.
|
|
1217
|
+
*/
|
|
1218
|
+
recordTool(tool, costUsd, options = {}) {
|
|
1219
|
+
return this.settleTool(tool, costUsd, { reserved: 0, label: options.label });
|
|
1220
|
+
}
|
|
1154
1221
|
/**
|
|
1155
1222
|
* Drop an in-flight reservation without recording spend (e.g. the call failed
|
|
1156
1223
|
* before producing usage). Safe to call with `0`.
|
|
@@ -1160,16 +1227,25 @@ var BudgetGuard = class {
|
|
|
1160
1227
|
throw new RangeError(`reserved must be a finite, non-negative number, got ${reserved}`);
|
|
1161
1228
|
}
|
|
1162
1229
|
if (!reserved) return;
|
|
1163
|
-
this.
|
|
1230
|
+
this.consumeReservation(reserved);
|
|
1164
1231
|
}
|
|
1165
1232
|
/** USD left before the ceiling, net of in-flight reservations (never negative). */
|
|
1166
1233
|
get remainingUsd() {
|
|
1167
1234
|
return Math.max(0, this.limitUsd - this.spentUsd - this.reserved);
|
|
1168
1235
|
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Per-tool running USD totals, keyed by the name given to `settleTool()` /
|
|
1238
|
+
* `recordTool()` — e.g. `{"apollo.people_lookup": 0.42, "exa.search": 0.11}`.
|
|
1239
|
+
* Makes the token/tool split of the one shared ceiling inspectable
|
|
1240
|
+
* (`spentUsd - sum of toolCosts` is the token side). Returns a snapshot copy.
|
|
1241
|
+
*/
|
|
1242
|
+
get toolCosts() {
|
|
1243
|
+
return { ...this.toolCostTotals };
|
|
1244
|
+
}
|
|
1169
1245
|
/**
|
|
1170
1246
|
* The per-call spend ledger, oldest first — one {@link SpendEvent} per priced
|
|
1171
|
-
* `record()` / `settle()` / `recordTool()`. Returns a
|
|
1172
|
-
* it cannot corrupt the ledger.
|
|
1247
|
+
* `record()` / `settle()` / `recordTool()` / `settleTool()`. Returns a
|
|
1248
|
+
* snapshot copy: mutating it cannot corrupt the ledger.
|
|
1173
1249
|
*/
|
|
1174
1250
|
get spendLog() {
|
|
1175
1251
|
return [...this.spendEvents];
|
|
@@ -1200,6 +1276,34 @@ var BudgetGuard = class {
|
|
|
1200
1276
|
`;
|
|
1201
1277
|
}).join("");
|
|
1202
1278
|
}
|
|
1279
|
+
/**
|
|
1280
|
+
* The default next-call prediction when the caller supplies no estimate.
|
|
1281
|
+
* Conservative: the costlier of the last LLM call and the last tool call — a
|
|
1282
|
+
* mixed loop predicts the pricier kind, which at worst blocks one call early
|
|
1283
|
+
* (fail-closed) rather than letting a crossing call through because the LAST
|
|
1284
|
+
* event happened to be cheap.
|
|
1285
|
+
*/
|
|
1286
|
+
defaultEstimate() {
|
|
1287
|
+
return Math.max(this.lastLlmCost, this.lastToolCost);
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Subtract a settled/released hold from the in-flight tally. A handle larger
|
|
1291
|
+
* than EVERYTHING currently held cannot have come from a matching
|
|
1292
|
+
* `reserve()` — throwing beats silently clamping, which would free OTHER
|
|
1293
|
+
* callers' holds and fail the ceiling open. The epsilon absorbs float dust
|
|
1294
|
+
* from accumulating and draining many holds; per-caller over-release (a
|
|
1295
|
+
* handle within the total but larger than the caller's own hold) is
|
|
1296
|
+
* undetectable without per-handle tracking and remains the caller's
|
|
1297
|
+
* responsibility.
|
|
1298
|
+
*/
|
|
1299
|
+
consumeReservation(reserved) {
|
|
1300
|
+
if (reserved > this.reserved + EPS) {
|
|
1301
|
+
throw new RangeError(
|
|
1302
|
+
`reserved handle (${reserved}) exceeds total in-flight reservations (${this.reserved}) \u2014 a handle must come from a matching reserve()`
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
this.reserved = Math.max(0, this.reserved - reserved);
|
|
1306
|
+
}
|
|
1203
1307
|
appendEvent(event) {
|
|
1204
1308
|
this.spendEvents.push(Object.freeze(event));
|
|
1205
1309
|
if (this.maxLogEvents !== void 0 && this.spendEvents.length > this.maxLogEvents) {
|