floe-guard 0.2.1 → 0.4.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 +21 -0
- package/dist/index.cjs +178 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +151 -2
- package/dist/index.d.ts +151 -2
- package/dist/index.js +176 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -64,6 +64,32 @@ declare namespace pricing {
|
|
|
64
64
|
* same epsilon handling, same fail-closed default.
|
|
65
65
|
*/
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* One priced spend event in the guard's per-call ledger.
|
|
69
|
+
*
|
|
70
|
+
* Every {@link BudgetGuard.record} / {@link BudgetGuard.settle} /
|
|
71
|
+
* {@link BudgetGuard.recordTool} that accrues spend appends exactly one event, so
|
|
72
|
+
* the ledger's costs sum to `spentUsd` (unless a `maxLogEvents` ring buffer has
|
|
73
|
+
* evicted old events). The schema is identical in the Python
|
|
74
|
+
* package (`SpendEvent` in `src/floe_guard/guard.py`) and
|
|
75
|
+
* {@link BudgetGuard.exportLog} serialises it with the same snake_case keys in
|
|
76
|
+
* both languages, so every agent emits the same shape regardless of stack.
|
|
77
|
+
*/
|
|
78
|
+
interface SpendEvent {
|
|
79
|
+
/** Unix epoch seconds (UTC). */
|
|
80
|
+
readonly timestamp: number;
|
|
81
|
+
readonly kind: "llm" | "tool";
|
|
82
|
+
readonly modelOrTool: string;
|
|
83
|
+
/** `null` for tool events. */
|
|
84
|
+
readonly promptTokens: number | null;
|
|
85
|
+
/** `null` for tool events. */
|
|
86
|
+
readonly completionTokens: number | null;
|
|
87
|
+
readonly costUsd: number;
|
|
88
|
+
/** Caller-supplied tag (agent/task name). */
|
|
89
|
+
readonly label?: string;
|
|
90
|
+
/** The reservation settled by this call, if any. */
|
|
91
|
+
readonly reserved?: number;
|
|
92
|
+
}
|
|
67
93
|
interface BudgetGuardOptions {
|
|
68
94
|
/** Per-model manual prices for models the bundled cost map cannot price. */
|
|
69
95
|
priceOverrides?: Record<string, ManualPrice>;
|
|
@@ -84,6 +110,13 @@ interface BudgetGuardOptions {
|
|
|
84
110
|
* flags `nearLimit` so an agent can taper before the hard-stop. Default 8000.
|
|
85
111
|
*/
|
|
86
112
|
nearLimitBps?: number;
|
|
113
|
+
/**
|
|
114
|
+
* Optional cap on the per-call spend ledger ({@link BudgetGuard.spendLog}).
|
|
115
|
+
* When set, the ledger is a ring buffer keeping the most recent N events so a
|
|
116
|
+
* long-running agent's memory stays bounded; the running totals are
|
|
117
|
+
* unaffected. Default: keep every event.
|
|
118
|
+
*/
|
|
119
|
+
maxLogEvents?: number;
|
|
87
120
|
}
|
|
88
121
|
/**
|
|
89
122
|
* A context-aware spend signal for the single local budget.
|
|
@@ -120,6 +153,9 @@ declare class BudgetGuard {
|
|
|
120
153
|
private lastCost;
|
|
121
154
|
/** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */
|
|
122
155
|
private reserved;
|
|
156
|
+
/** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */
|
|
157
|
+
private readonly spendEvents;
|
|
158
|
+
private readonly maxLogEvents?;
|
|
123
159
|
/**
|
|
124
160
|
* @param limitUsd the spend ceiling, in USD. `0` blocks the very first call.
|
|
125
161
|
*/
|
|
@@ -152,11 +188,15 @@ declare class BudgetGuard {
|
|
|
152
188
|
* Release a reservation and record the actual cost. `record` is `settle` with
|
|
153
189
|
* no reservation. Returns the USD cost of this call; unpriceable-model handling
|
|
154
190
|
* matches {@link BudgetGuard.record}, and any held reservation is released even
|
|
155
|
-
* on the warn-and-skip path.
|
|
191
|
+
* on the warn-and-skip path. A priced call appends one {@link SpendEvent} to
|
|
192
|
+
* {@link BudgetGuard.spendLog} (`label` tags it, e.g. with an agent/task name);
|
|
193
|
+
* the warn-and-skip path accrues nothing and logs nothing, so the ledger stays
|
|
194
|
+
* in lockstep with `spentUsd`.
|
|
156
195
|
*/
|
|
157
196
|
settle(model: string, promptTokens: number, completionTokens: number, options?: {
|
|
158
197
|
reserved?: number;
|
|
159
198
|
price?: ManualPrice;
|
|
199
|
+
label?: string;
|
|
160
200
|
}): number;
|
|
161
201
|
/**
|
|
162
202
|
* Price one response's tokens offline and add the cost to the total.
|
|
@@ -167,6 +207,21 @@ declare class BudgetGuard {
|
|
|
167
207
|
*/
|
|
168
208
|
record(model: string, promptTokens: number, completionTokens: number, options?: {
|
|
169
209
|
price?: ManualPrice;
|
|
210
|
+
label?: string;
|
|
211
|
+
}): number;
|
|
212
|
+
/**
|
|
213
|
+
* Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.
|
|
214
|
+
*
|
|
215
|
+
* Tools with direct dollar costs — search APIs, scrapers, sandboxes — spend the
|
|
216
|
+
* same budget the LLM calls do; `recordTool` folds them into `spentUsd` (so
|
|
217
|
+
* `check()` / `reserve()` see them) and appends a `kind: "tool"`
|
|
218
|
+
* {@link SpendEvent} to {@link BudgetGuard.spendLog}. The caller supplies the
|
|
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`.
|
|
222
|
+
*/
|
|
223
|
+
recordTool(tool: string, costUsd: number, options?: {
|
|
224
|
+
label?: string;
|
|
170
225
|
}): number;
|
|
171
226
|
/**
|
|
172
227
|
* Drop an in-flight reservation without recording spend (e.g. the call failed
|
|
@@ -175,6 +230,24 @@ declare class BudgetGuard {
|
|
|
175
230
|
release(reserved: number): void;
|
|
176
231
|
/** USD left before the ceiling, net of in-flight reservations (never negative). */
|
|
177
232
|
get remainingUsd(): number;
|
|
233
|
+
/**
|
|
234
|
+
* The per-call spend ledger, oldest first — one {@link SpendEvent} per priced
|
|
235
|
+
* `record()` / `settle()` / `recordTool()`. Returns a snapshot copy: mutating
|
|
236
|
+
* it cannot corrupt the ledger.
|
|
237
|
+
*/
|
|
238
|
+
get spendLog(): SpendEvent[];
|
|
239
|
+
/**
|
|
240
|
+
* The spend ledger as JSONL — one event per line, newline-terminated.
|
|
241
|
+
*
|
|
242
|
+
* The schema is stable and language-independent (snake_case keys, fixed order;
|
|
243
|
+
* optional fields omitted when absent), identical to the Python package's
|
|
244
|
+
* `export_log()`, so heterogeneous agents produce logs you can concatenate and
|
|
245
|
+
* analyse as one stream. (The *schema* is the contract, not the bytes: the two
|
|
246
|
+
* runtimes may render the same float differently, e.g. JS `0.0000025` vs
|
|
247
|
+
* Python `2.5e-06`.) Empty ledger yields `""`.
|
|
248
|
+
*/
|
|
249
|
+
exportLog(): string;
|
|
250
|
+
private appendEvent;
|
|
178
251
|
/**
|
|
179
252
|
* Context-aware spend advisory for this budget — see {@link BudgetAdvisory}.
|
|
180
253
|
*
|
|
@@ -185,6 +258,77 @@ declare class BudgetGuard {
|
|
|
185
258
|
advisory(): BudgetAdvisory;
|
|
186
259
|
}
|
|
187
260
|
|
|
261
|
+
/**
|
|
262
|
+
* LatencyBudget — a cumulative tool-chain deadline, sibling to BudgetGuard.
|
|
263
|
+
*
|
|
264
|
+
* BudgetGuard stops an agent before its next call crosses a USD ceiling;
|
|
265
|
+
* LatencyBudget stops it before the next call would blow an end-user SLA:
|
|
266
|
+
*
|
|
267
|
+
* ```ts
|
|
268
|
+
* const deadline = new LatencyBudget(5000);
|
|
269
|
+
* ...
|
|
270
|
+
* deadline.check(800); // throws DeadlineExceeded when projected over
|
|
271
|
+
* if (deadline.advisory().nearDeadline) useFasterModel();
|
|
272
|
+
* router.pick({ maxLatencyMs: deadline.remainingMs });
|
|
273
|
+
* ```
|
|
274
|
+
*
|
|
275
|
+
* Design notes (mirroring `src/floe_guard/latency.py`):
|
|
276
|
+
* - **Monotonic clock** — `performance.now()`, never wall time.
|
|
277
|
+
* - **Cooperative, not preemptive** — the guard provides the deadline signal;
|
|
278
|
+
* aborting a stalled in-flight call is the framework's job (AbortSignal).
|
|
279
|
+
* `check()` prevents the NEXT call from starting.
|
|
280
|
+
* - **Advisory symmetry** — `nearDeadline` / `usedBps` / `remainingMs` are the
|
|
281
|
+
* latency twin of BudgetGuard's `nearLimit` / `usedBps` / `remainingUsd`.
|
|
282
|
+
* - **In-process scope** — one instance per request/run; distributed latency
|
|
283
|
+
* tracking is out of scope.
|
|
284
|
+
*/
|
|
285
|
+
/** A context-aware deadline signal — the latency twin of {@link BudgetAdvisory}.
|
|
286
|
+
* Soft by design; the hard-stop is {@link LatencyBudget.check}. */
|
|
287
|
+
interface LatencyAdvisory {
|
|
288
|
+
nearDeadline: boolean;
|
|
289
|
+
/** SLA consumed, basis points 0..10000 (8500 = 85%). */
|
|
290
|
+
usedBps: number;
|
|
291
|
+
remainingMs: number;
|
|
292
|
+
slaMs: number;
|
|
293
|
+
elapsedMs: number;
|
|
294
|
+
}
|
|
295
|
+
interface LatencyBudgetOptions {
|
|
296
|
+
/**
|
|
297
|
+
* Utilization (basis points, 0..10000) at which {@link LatencyBudget.advisory}
|
|
298
|
+
* flags `nearDeadline` so an agent can downshift to a faster path before the
|
|
299
|
+
* wall. Default 8000 (80%), matching BudgetGuard's `nearLimitBps`.
|
|
300
|
+
*/
|
|
301
|
+
nearDeadlineBps?: number;
|
|
302
|
+
/** Invoked with `(elapsedMs, slaMs)` right before {@link DeadlineExceeded} is thrown. */
|
|
303
|
+
onBlock?: (elapsedMs: number, slaMs: number) => void;
|
|
304
|
+
/** Milliseconds-returning monotonic clock, injectable for tests. Defaults to `performance.now`. */
|
|
305
|
+
clock?: () => number;
|
|
306
|
+
}
|
|
307
|
+
declare class LatencyBudget {
|
|
308
|
+
readonly slaMs: number;
|
|
309
|
+
readonly nearDeadlineBps: number;
|
|
310
|
+
private readonly onBlock?;
|
|
311
|
+
private readonly clock;
|
|
312
|
+
private readonly startedAt;
|
|
313
|
+
/** The budget starts counting at construction — build it when the request
|
|
314
|
+
* (and its SLA) starts. */
|
|
315
|
+
constructor(slaMs: number, options?: LatencyBudgetOptions);
|
|
316
|
+
/** Milliseconds since construction (monotonic). */
|
|
317
|
+
get elapsedMs(): number;
|
|
318
|
+
/** Milliseconds left before the SLA, floored at 0 — the readable signal a
|
|
319
|
+
* router uses to pick a faster fallback or truncate work mid-chain. */
|
|
320
|
+
get remainingMs(): number;
|
|
321
|
+
/**
|
|
322
|
+
* Throw {@link DeadlineExceeded} when the projected elapsed time (now +
|
|
323
|
+
* `expectedMs` for the upcoming call) would blow the SLA. Call it
|
|
324
|
+
* immediately before each tool/model call; pass 0 to only gate on time
|
|
325
|
+
* already spent.
|
|
326
|
+
*/
|
|
327
|
+
check(expectedMs?: number): void;
|
|
328
|
+
/** The soft near-deadline signal — symmetric to `BudgetGuard.advisory()`. */
|
|
329
|
+
advisory(): LatencyAdvisory;
|
|
330
|
+
}
|
|
331
|
+
|
|
188
332
|
/**
|
|
189
333
|
* Exceptions for floe-guard.
|
|
190
334
|
*
|
|
@@ -220,6 +364,11 @@ declare class UnpriceableModelError extends FloeGuardError {
|
|
|
220
364
|
readonly model: string;
|
|
221
365
|
constructor(model: string);
|
|
222
366
|
}
|
|
367
|
+
declare class DeadlineExceeded extends FloeGuardError {
|
|
368
|
+
readonly elapsedMs: number;
|
|
369
|
+
readonly slaMs: number;
|
|
370
|
+
constructor(elapsedMs: number, slaMs: number);
|
|
371
|
+
}
|
|
223
372
|
|
|
224
373
|
/**
|
|
225
374
|
* Vercel AI SDK middleware that enforces a {@link BudgetGuard} in the call path.
|
|
@@ -290,4 +439,4 @@ interface BudgetGuardMiddleware {
|
|
|
290
439
|
*/
|
|
291
440
|
declare function budgetGuardMiddleware(guard: BudgetGuard): BudgetGuardMiddleware;
|
|
292
441
|
|
|
293
|
-
export { type BudgetAdvisory, BudgetExceeded, BudgetGuard, type BudgetGuardMiddleware, type BudgetGuardOptions, FloeGuardError, type ManualPrice, type PricedModel, UnpriceableModelError, budgetGuardMiddleware, priceTokens, pricing, resolvePrice };
|
|
442
|
+
export { type BudgetAdvisory, BudgetExceeded, BudgetGuard, type BudgetGuardMiddleware, type BudgetGuardOptions, DeadlineExceeded, FloeGuardError, type LatencyAdvisory, LatencyBudget, type LatencyBudgetOptions, type ManualPrice, type PricedModel, type SpendEvent, UnpriceableModelError, budgetGuardMiddleware, priceTokens, pricing, resolvePrice };
|
package/dist/index.d.ts
CHANGED
|
@@ -64,6 +64,32 @@ declare namespace pricing {
|
|
|
64
64
|
* same epsilon handling, same fail-closed default.
|
|
65
65
|
*/
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* One priced spend event in the guard's per-call ledger.
|
|
69
|
+
*
|
|
70
|
+
* Every {@link BudgetGuard.record} / {@link BudgetGuard.settle} /
|
|
71
|
+
* {@link BudgetGuard.recordTool} that accrues spend appends exactly one event, so
|
|
72
|
+
* the ledger's costs sum to `spentUsd` (unless a `maxLogEvents` ring buffer has
|
|
73
|
+
* evicted old events). The schema is identical in the Python
|
|
74
|
+
* package (`SpendEvent` in `src/floe_guard/guard.py`) and
|
|
75
|
+
* {@link BudgetGuard.exportLog} serialises it with the same snake_case keys in
|
|
76
|
+
* both languages, so every agent emits the same shape regardless of stack.
|
|
77
|
+
*/
|
|
78
|
+
interface SpendEvent {
|
|
79
|
+
/** Unix epoch seconds (UTC). */
|
|
80
|
+
readonly timestamp: number;
|
|
81
|
+
readonly kind: "llm" | "tool";
|
|
82
|
+
readonly modelOrTool: string;
|
|
83
|
+
/** `null` for tool events. */
|
|
84
|
+
readonly promptTokens: number | null;
|
|
85
|
+
/** `null` for tool events. */
|
|
86
|
+
readonly completionTokens: number | null;
|
|
87
|
+
readonly costUsd: number;
|
|
88
|
+
/** Caller-supplied tag (agent/task name). */
|
|
89
|
+
readonly label?: string;
|
|
90
|
+
/** The reservation settled by this call, if any. */
|
|
91
|
+
readonly reserved?: number;
|
|
92
|
+
}
|
|
67
93
|
interface BudgetGuardOptions {
|
|
68
94
|
/** Per-model manual prices for models the bundled cost map cannot price. */
|
|
69
95
|
priceOverrides?: Record<string, ManualPrice>;
|
|
@@ -84,6 +110,13 @@ interface BudgetGuardOptions {
|
|
|
84
110
|
* flags `nearLimit` so an agent can taper before the hard-stop. Default 8000.
|
|
85
111
|
*/
|
|
86
112
|
nearLimitBps?: number;
|
|
113
|
+
/**
|
|
114
|
+
* Optional cap on the per-call spend ledger ({@link BudgetGuard.spendLog}).
|
|
115
|
+
* When set, the ledger is a ring buffer keeping the most recent N events so a
|
|
116
|
+
* long-running agent's memory stays bounded; the running totals are
|
|
117
|
+
* unaffected. Default: keep every event.
|
|
118
|
+
*/
|
|
119
|
+
maxLogEvents?: number;
|
|
87
120
|
}
|
|
88
121
|
/**
|
|
89
122
|
* A context-aware spend signal for the single local budget.
|
|
@@ -120,6 +153,9 @@ declare class BudgetGuard {
|
|
|
120
153
|
private lastCost;
|
|
121
154
|
/** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */
|
|
122
155
|
private reserved;
|
|
156
|
+
/** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */
|
|
157
|
+
private readonly spendEvents;
|
|
158
|
+
private readonly maxLogEvents?;
|
|
123
159
|
/**
|
|
124
160
|
* @param limitUsd the spend ceiling, in USD. `0` blocks the very first call.
|
|
125
161
|
*/
|
|
@@ -152,11 +188,15 @@ declare class BudgetGuard {
|
|
|
152
188
|
* Release a reservation and record the actual cost. `record` is `settle` with
|
|
153
189
|
* no reservation. Returns the USD cost of this call; unpriceable-model handling
|
|
154
190
|
* matches {@link BudgetGuard.record}, and any held reservation is released even
|
|
155
|
-
* on the warn-and-skip path.
|
|
191
|
+
* on the warn-and-skip path. A priced call appends one {@link SpendEvent} to
|
|
192
|
+
* {@link BudgetGuard.spendLog} (`label` tags it, e.g. with an agent/task name);
|
|
193
|
+
* the warn-and-skip path accrues nothing and logs nothing, so the ledger stays
|
|
194
|
+
* in lockstep with `spentUsd`.
|
|
156
195
|
*/
|
|
157
196
|
settle(model: string, promptTokens: number, completionTokens: number, options?: {
|
|
158
197
|
reserved?: number;
|
|
159
198
|
price?: ManualPrice;
|
|
199
|
+
label?: string;
|
|
160
200
|
}): number;
|
|
161
201
|
/**
|
|
162
202
|
* Price one response's tokens offline and add the cost to the total.
|
|
@@ -167,6 +207,21 @@ declare class BudgetGuard {
|
|
|
167
207
|
*/
|
|
168
208
|
record(model: string, promptTokens: number, completionTokens: number, options?: {
|
|
169
209
|
price?: ManualPrice;
|
|
210
|
+
label?: string;
|
|
211
|
+
}): number;
|
|
212
|
+
/**
|
|
213
|
+
* Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.
|
|
214
|
+
*
|
|
215
|
+
* Tools with direct dollar costs — search APIs, scrapers, sandboxes — spend the
|
|
216
|
+
* same budget the LLM calls do; `recordTool` folds them into `spentUsd` (so
|
|
217
|
+
* `check()` / `reserve()` see them) and appends a `kind: "tool"`
|
|
218
|
+
* {@link SpendEvent} to {@link BudgetGuard.spendLog}. The caller supplies the
|
|
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`.
|
|
222
|
+
*/
|
|
223
|
+
recordTool(tool: string, costUsd: number, options?: {
|
|
224
|
+
label?: string;
|
|
170
225
|
}): number;
|
|
171
226
|
/**
|
|
172
227
|
* Drop an in-flight reservation without recording spend (e.g. the call failed
|
|
@@ -175,6 +230,24 @@ declare class BudgetGuard {
|
|
|
175
230
|
release(reserved: number): void;
|
|
176
231
|
/** USD left before the ceiling, net of in-flight reservations (never negative). */
|
|
177
232
|
get remainingUsd(): number;
|
|
233
|
+
/**
|
|
234
|
+
* The per-call spend ledger, oldest first — one {@link SpendEvent} per priced
|
|
235
|
+
* `record()` / `settle()` / `recordTool()`. Returns a snapshot copy: mutating
|
|
236
|
+
* it cannot corrupt the ledger.
|
|
237
|
+
*/
|
|
238
|
+
get spendLog(): SpendEvent[];
|
|
239
|
+
/**
|
|
240
|
+
* The spend ledger as JSONL — one event per line, newline-terminated.
|
|
241
|
+
*
|
|
242
|
+
* The schema is stable and language-independent (snake_case keys, fixed order;
|
|
243
|
+
* optional fields omitted when absent), identical to the Python package's
|
|
244
|
+
* `export_log()`, so heterogeneous agents produce logs you can concatenate and
|
|
245
|
+
* analyse as one stream. (The *schema* is the contract, not the bytes: the two
|
|
246
|
+
* runtimes may render the same float differently, e.g. JS `0.0000025` vs
|
|
247
|
+
* Python `2.5e-06`.) Empty ledger yields `""`.
|
|
248
|
+
*/
|
|
249
|
+
exportLog(): string;
|
|
250
|
+
private appendEvent;
|
|
178
251
|
/**
|
|
179
252
|
* Context-aware spend advisory for this budget — see {@link BudgetAdvisory}.
|
|
180
253
|
*
|
|
@@ -185,6 +258,77 @@ declare class BudgetGuard {
|
|
|
185
258
|
advisory(): BudgetAdvisory;
|
|
186
259
|
}
|
|
187
260
|
|
|
261
|
+
/**
|
|
262
|
+
* LatencyBudget — a cumulative tool-chain deadline, sibling to BudgetGuard.
|
|
263
|
+
*
|
|
264
|
+
* BudgetGuard stops an agent before its next call crosses a USD ceiling;
|
|
265
|
+
* LatencyBudget stops it before the next call would blow an end-user SLA:
|
|
266
|
+
*
|
|
267
|
+
* ```ts
|
|
268
|
+
* const deadline = new LatencyBudget(5000);
|
|
269
|
+
* ...
|
|
270
|
+
* deadline.check(800); // throws DeadlineExceeded when projected over
|
|
271
|
+
* if (deadline.advisory().nearDeadline) useFasterModel();
|
|
272
|
+
* router.pick({ maxLatencyMs: deadline.remainingMs });
|
|
273
|
+
* ```
|
|
274
|
+
*
|
|
275
|
+
* Design notes (mirroring `src/floe_guard/latency.py`):
|
|
276
|
+
* - **Monotonic clock** — `performance.now()`, never wall time.
|
|
277
|
+
* - **Cooperative, not preemptive** — the guard provides the deadline signal;
|
|
278
|
+
* aborting a stalled in-flight call is the framework's job (AbortSignal).
|
|
279
|
+
* `check()` prevents the NEXT call from starting.
|
|
280
|
+
* - **Advisory symmetry** — `nearDeadline` / `usedBps` / `remainingMs` are the
|
|
281
|
+
* latency twin of BudgetGuard's `nearLimit` / `usedBps` / `remainingUsd`.
|
|
282
|
+
* - **In-process scope** — one instance per request/run; distributed latency
|
|
283
|
+
* tracking is out of scope.
|
|
284
|
+
*/
|
|
285
|
+
/** A context-aware deadline signal — the latency twin of {@link BudgetAdvisory}.
|
|
286
|
+
* Soft by design; the hard-stop is {@link LatencyBudget.check}. */
|
|
287
|
+
interface LatencyAdvisory {
|
|
288
|
+
nearDeadline: boolean;
|
|
289
|
+
/** SLA consumed, basis points 0..10000 (8500 = 85%). */
|
|
290
|
+
usedBps: number;
|
|
291
|
+
remainingMs: number;
|
|
292
|
+
slaMs: number;
|
|
293
|
+
elapsedMs: number;
|
|
294
|
+
}
|
|
295
|
+
interface LatencyBudgetOptions {
|
|
296
|
+
/**
|
|
297
|
+
* Utilization (basis points, 0..10000) at which {@link LatencyBudget.advisory}
|
|
298
|
+
* flags `nearDeadline` so an agent can downshift to a faster path before the
|
|
299
|
+
* wall. Default 8000 (80%), matching BudgetGuard's `nearLimitBps`.
|
|
300
|
+
*/
|
|
301
|
+
nearDeadlineBps?: number;
|
|
302
|
+
/** Invoked with `(elapsedMs, slaMs)` right before {@link DeadlineExceeded} is thrown. */
|
|
303
|
+
onBlock?: (elapsedMs: number, slaMs: number) => void;
|
|
304
|
+
/** Milliseconds-returning monotonic clock, injectable for tests. Defaults to `performance.now`. */
|
|
305
|
+
clock?: () => number;
|
|
306
|
+
}
|
|
307
|
+
declare class LatencyBudget {
|
|
308
|
+
readonly slaMs: number;
|
|
309
|
+
readonly nearDeadlineBps: number;
|
|
310
|
+
private readonly onBlock?;
|
|
311
|
+
private readonly clock;
|
|
312
|
+
private readonly startedAt;
|
|
313
|
+
/** The budget starts counting at construction — build it when the request
|
|
314
|
+
* (and its SLA) starts. */
|
|
315
|
+
constructor(slaMs: number, options?: LatencyBudgetOptions);
|
|
316
|
+
/** Milliseconds since construction (monotonic). */
|
|
317
|
+
get elapsedMs(): number;
|
|
318
|
+
/** Milliseconds left before the SLA, floored at 0 — the readable signal a
|
|
319
|
+
* router uses to pick a faster fallback or truncate work mid-chain. */
|
|
320
|
+
get remainingMs(): number;
|
|
321
|
+
/**
|
|
322
|
+
* Throw {@link DeadlineExceeded} when the projected elapsed time (now +
|
|
323
|
+
* `expectedMs` for the upcoming call) would blow the SLA. Call it
|
|
324
|
+
* immediately before each tool/model call; pass 0 to only gate on time
|
|
325
|
+
* already spent.
|
|
326
|
+
*/
|
|
327
|
+
check(expectedMs?: number): void;
|
|
328
|
+
/** The soft near-deadline signal — symmetric to `BudgetGuard.advisory()`. */
|
|
329
|
+
advisory(): LatencyAdvisory;
|
|
330
|
+
}
|
|
331
|
+
|
|
188
332
|
/**
|
|
189
333
|
* Exceptions for floe-guard.
|
|
190
334
|
*
|
|
@@ -220,6 +364,11 @@ declare class UnpriceableModelError extends FloeGuardError {
|
|
|
220
364
|
readonly model: string;
|
|
221
365
|
constructor(model: string);
|
|
222
366
|
}
|
|
367
|
+
declare class DeadlineExceeded extends FloeGuardError {
|
|
368
|
+
readonly elapsedMs: number;
|
|
369
|
+
readonly slaMs: number;
|
|
370
|
+
constructor(elapsedMs: number, slaMs: number);
|
|
371
|
+
}
|
|
223
372
|
|
|
224
373
|
/**
|
|
225
374
|
* Vercel AI SDK middleware that enforces a {@link BudgetGuard} in the call path.
|
|
@@ -290,4 +439,4 @@ interface BudgetGuardMiddleware {
|
|
|
290
439
|
*/
|
|
291
440
|
declare function budgetGuardMiddleware(guard: BudgetGuard): BudgetGuardMiddleware;
|
|
292
441
|
|
|
293
|
-
export { type BudgetAdvisory, BudgetExceeded, BudgetGuard, type BudgetGuardMiddleware, type BudgetGuardOptions, FloeGuardError, type ManualPrice, type PricedModel, UnpriceableModelError, budgetGuardMiddleware, priceTokens, pricing, resolvePrice };
|
|
442
|
+
export { type BudgetAdvisory, BudgetExceeded, BudgetGuard, type BudgetGuardMiddleware, type BudgetGuardOptions, DeadlineExceeded, FloeGuardError, type LatencyAdvisory, LatencyBudget, type LatencyBudgetOptions, type ManualPrice, type PricedModel, type SpendEvent, UnpriceableModelError, budgetGuardMiddleware, priceTokens, pricing, resolvePrice };
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,21 @@ var UnpriceableModelError = class extends FloeGuardError {
|
|
|
34
34
|
this.model = model;
|
|
35
35
|
}
|
|
36
36
|
};
|
|
37
|
+
function roundHalfUp(ms) {
|
|
38
|
+
return Math.floor(ms + 0.5);
|
|
39
|
+
}
|
|
40
|
+
var DeadlineExceeded = class extends FloeGuardError {
|
|
41
|
+
elapsedMs;
|
|
42
|
+
slaMs;
|
|
43
|
+
constructor(elapsedMs, slaMs) {
|
|
44
|
+
super(
|
|
45
|
+
`DEADLINE EXCEEDED \u2014 call blocked (elapsed ${roundHalfUp(elapsedMs)}ms of ${roundHalfUp(slaMs)}ms SLA)`
|
|
46
|
+
);
|
|
47
|
+
this.name = "DeadlineExceeded";
|
|
48
|
+
this.elapsedMs = elapsedMs;
|
|
49
|
+
this.slaMs = slaMs;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
37
52
|
|
|
38
53
|
// src/pricing.ts
|
|
39
54
|
var pricing_exports = {};
|
|
@@ -952,6 +967,9 @@ var BudgetGuard = class {
|
|
|
952
967
|
lastCost = 0;
|
|
953
968
|
/** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */
|
|
954
969
|
reserved = 0;
|
|
970
|
+
/** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */
|
|
971
|
+
spendEvents = [];
|
|
972
|
+
maxLogEvents;
|
|
955
973
|
/**
|
|
956
974
|
* @param limitUsd the spend ceiling, in USD. `0` blocks the very first call.
|
|
957
975
|
*/
|
|
@@ -967,7 +985,13 @@ var BudgetGuard = class {
|
|
|
967
985
|
`nearLimitBps must be an integer in 0..10000, got ${nearLimitBps}`
|
|
968
986
|
);
|
|
969
987
|
}
|
|
988
|
+
if (options.maxLogEvents !== void 0 && (!Number.isInteger(options.maxLogEvents) || options.maxLogEvents < 0)) {
|
|
989
|
+
throw new RangeError(
|
|
990
|
+
`maxLogEvents must be a non-negative integer, got ${options.maxLogEvents}`
|
|
991
|
+
);
|
|
992
|
+
}
|
|
970
993
|
this.limitUsd = limitUsd;
|
|
994
|
+
this.maxLogEvents = options.maxLogEvents;
|
|
971
995
|
this.priceOverrides = options.priceOverrides;
|
|
972
996
|
this.failClosed = options.failClosed ?? true;
|
|
973
997
|
this.onBlock = options.onBlock ?? defaultOnBlock;
|
|
@@ -1029,7 +1053,10 @@ var BudgetGuard = class {
|
|
|
1029
1053
|
* Release a reservation and record the actual cost. `record` is `settle` with
|
|
1030
1054
|
* no reservation. Returns the USD cost of this call; unpriceable-model handling
|
|
1031
1055
|
* matches {@link BudgetGuard.record}, and any held reservation is released even
|
|
1032
|
-
* on the warn-and-skip path.
|
|
1056
|
+
* on the warn-and-skip path. A priced call appends one {@link SpendEvent} to
|
|
1057
|
+
* {@link BudgetGuard.spendLog} (`label` tags it, e.g. with an agent/task name);
|
|
1058
|
+
* the warn-and-skip path accrues nothing and logs nothing, so the ledger stays
|
|
1059
|
+
* in lockstep with `spentUsd`.
|
|
1033
1060
|
*/
|
|
1034
1061
|
settle(model, promptTokens, completionTokens, options = {}) {
|
|
1035
1062
|
const reserved = options.reserved ?? 0;
|
|
@@ -1066,6 +1093,18 @@ var BudgetGuard = class {
|
|
|
1066
1093
|
this.spentUsd = this.limitUsd;
|
|
1067
1094
|
}
|
|
1068
1095
|
this.lastCost = cost;
|
|
1096
|
+
this.appendEvent({
|
|
1097
|
+
timestamp: Date.now() / 1e3,
|
|
1098
|
+
kind: "llm",
|
|
1099
|
+
modelOrTool: model,
|
|
1100
|
+
promptTokens,
|
|
1101
|
+
completionTokens,
|
|
1102
|
+
costUsd: cost,
|
|
1103
|
+
...options.label !== void 0 ? { label: options.label } : {},
|
|
1104
|
+
// 0 means "no reservation" (the plain record() path) — omit rather than
|
|
1105
|
+
// log a meaningless zero.
|
|
1106
|
+
...reserved ? { reserved } : {}
|
|
1107
|
+
});
|
|
1069
1108
|
return cost;
|
|
1070
1109
|
}
|
|
1071
1110
|
/**
|
|
@@ -1078,9 +1117,40 @@ var BudgetGuard = class {
|
|
|
1078
1117
|
record(model, promptTokens, completionTokens, options = {}) {
|
|
1079
1118
|
return this.settle(model, promptTokens, completionTokens, {
|
|
1080
1119
|
reserved: 0,
|
|
1081
|
-
price: options.price
|
|
1120
|
+
price: options.price,
|
|
1121
|
+
label: options.label
|
|
1082
1122
|
});
|
|
1083
1123
|
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.
|
|
1126
|
+
*
|
|
1127
|
+
* Tools with direct dollar costs — search APIs, scrapers, sandboxes — spend the
|
|
1128
|
+
* same budget the LLM calls do; `recordTool` folds them into `spentUsd` (so
|
|
1129
|
+
* `check()` / `reserve()` see them) and appends a `kind: "tool"`
|
|
1130
|
+
* {@link SpendEvent} to {@link BudgetGuard.spendLog}. The caller supplies the
|
|
1131
|
+
* cost: tools have no token usage to price. Deliberately does NOT update the
|
|
1132
|
+
* next-call estimate — that predicts the next *LLM* call, and a tool's price
|
|
1133
|
+
* would skew it. Returns `costUsd`.
|
|
1134
|
+
*/
|
|
1135
|
+
recordTool(tool, costUsd, options = {}) {
|
|
1136
|
+
if (!Number.isFinite(costUsd) || costUsd < 0) {
|
|
1137
|
+
throw new RangeError(`costUsd must be a finite, non-negative number, got ${costUsd}`);
|
|
1138
|
+
}
|
|
1139
|
+
this.spentUsd += costUsd;
|
|
1140
|
+
if (this.spentUsd - this.limitUsd > 0 && this.spentUsd - this.limitUsd < EPS) {
|
|
1141
|
+
this.spentUsd = this.limitUsd;
|
|
1142
|
+
}
|
|
1143
|
+
this.appendEvent({
|
|
1144
|
+
timestamp: Date.now() / 1e3,
|
|
1145
|
+
kind: "tool",
|
|
1146
|
+
modelOrTool: tool,
|
|
1147
|
+
promptTokens: null,
|
|
1148
|
+
completionTokens: null,
|
|
1149
|
+
costUsd,
|
|
1150
|
+
...options.label !== void 0 ? { label: options.label } : {}
|
|
1151
|
+
});
|
|
1152
|
+
return costUsd;
|
|
1153
|
+
}
|
|
1084
1154
|
/**
|
|
1085
1155
|
* Drop an in-flight reservation without recording spend (e.g. the call failed
|
|
1086
1156
|
* before producing usage). Safe to call with `0`.
|
|
@@ -1096,6 +1166,46 @@ var BudgetGuard = class {
|
|
|
1096
1166
|
get remainingUsd() {
|
|
1097
1167
|
return Math.max(0, this.limitUsd - this.spentUsd - this.reserved);
|
|
1098
1168
|
}
|
|
1169
|
+
/**
|
|
1170
|
+
* The per-call spend ledger, oldest first — one {@link SpendEvent} per priced
|
|
1171
|
+
* `record()` / `settle()` / `recordTool()`. Returns a snapshot copy: mutating
|
|
1172
|
+
* it cannot corrupt the ledger.
|
|
1173
|
+
*/
|
|
1174
|
+
get spendLog() {
|
|
1175
|
+
return [...this.spendEvents];
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* The spend ledger as JSONL — one event per line, newline-terminated.
|
|
1179
|
+
*
|
|
1180
|
+
* The schema is stable and language-independent (snake_case keys, fixed order;
|
|
1181
|
+
* optional fields omitted when absent), identical to the Python package's
|
|
1182
|
+
* `export_log()`, so heterogeneous agents produce logs you can concatenate and
|
|
1183
|
+
* analyse as one stream. (The *schema* is the contract, not the bytes: the two
|
|
1184
|
+
* runtimes may render the same float differently, e.g. JS `0.0000025` vs
|
|
1185
|
+
* Python `2.5e-06`.) Empty ledger yields `""`.
|
|
1186
|
+
*/
|
|
1187
|
+
exportLog() {
|
|
1188
|
+
return this.spendEvents.map((e) => {
|
|
1189
|
+
const row = {
|
|
1190
|
+
timestamp: e.timestamp,
|
|
1191
|
+
kind: e.kind,
|
|
1192
|
+
model_or_tool: e.modelOrTool,
|
|
1193
|
+
prompt_tokens: e.promptTokens,
|
|
1194
|
+
completion_tokens: e.completionTokens,
|
|
1195
|
+
cost_usd: e.costUsd
|
|
1196
|
+
};
|
|
1197
|
+
if (e.label !== void 0) row.label = e.label;
|
|
1198
|
+
if (e.reserved !== void 0) row.reserved = e.reserved;
|
|
1199
|
+
return `${JSON.stringify(row)}
|
|
1200
|
+
`;
|
|
1201
|
+
}).join("");
|
|
1202
|
+
}
|
|
1203
|
+
appendEvent(event) {
|
|
1204
|
+
this.spendEvents.push(Object.freeze(event));
|
|
1205
|
+
if (this.maxLogEvents !== void 0 && this.spendEvents.length > this.maxLogEvents) {
|
|
1206
|
+
this.spendEvents.splice(0, this.spendEvents.length - this.maxLogEvents);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1099
1209
|
/**
|
|
1100
1210
|
* Context-aware spend advisory for this budget — see {@link BudgetAdvisory}.
|
|
1101
1211
|
*
|
|
@@ -1127,6 +1237,68 @@ function defaultOnBlock(spentUsd, limitUsd) {
|
|
|
1127
1237
|
);
|
|
1128
1238
|
}
|
|
1129
1239
|
|
|
1240
|
+
// src/latency.ts
|
|
1241
|
+
var LatencyBudget = class {
|
|
1242
|
+
slaMs;
|
|
1243
|
+
nearDeadlineBps;
|
|
1244
|
+
onBlock;
|
|
1245
|
+
clock;
|
|
1246
|
+
startedAt;
|
|
1247
|
+
/** The budget starts counting at construction — build it when the request
|
|
1248
|
+
* (and its SLA) starts. */
|
|
1249
|
+
constructor(slaMs, options = {}) {
|
|
1250
|
+
if (!(Number.isFinite(slaMs) && slaMs > 0)) {
|
|
1251
|
+
throw new RangeError("slaMs must be > 0");
|
|
1252
|
+
}
|
|
1253
|
+
const nearDeadlineBps = options.nearDeadlineBps ?? 8e3;
|
|
1254
|
+
if (!Number.isInteger(nearDeadlineBps) || nearDeadlineBps < 0 || nearDeadlineBps > 1e4) {
|
|
1255
|
+
throw new RangeError("nearDeadlineBps must be an integer 0..10000");
|
|
1256
|
+
}
|
|
1257
|
+
this.slaMs = slaMs;
|
|
1258
|
+
this.nearDeadlineBps = nearDeadlineBps;
|
|
1259
|
+
this.onBlock = options.onBlock;
|
|
1260
|
+
this.clock = options.clock ?? (() => performance.now());
|
|
1261
|
+
this.startedAt = this.clock();
|
|
1262
|
+
}
|
|
1263
|
+
/** Milliseconds since construction (monotonic). */
|
|
1264
|
+
get elapsedMs() {
|
|
1265
|
+
return this.clock() - this.startedAt;
|
|
1266
|
+
}
|
|
1267
|
+
/** Milliseconds left before the SLA, floored at 0 — the readable signal a
|
|
1268
|
+
* router uses to pick a faster fallback or truncate work mid-chain. */
|
|
1269
|
+
get remainingMs() {
|
|
1270
|
+
return Math.max(0, this.slaMs - this.elapsedMs);
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Throw {@link DeadlineExceeded} when the projected elapsed time (now +
|
|
1274
|
+
* `expectedMs` for the upcoming call) would blow the SLA. Call it
|
|
1275
|
+
* immediately before each tool/model call; pass 0 to only gate on time
|
|
1276
|
+
* already spent.
|
|
1277
|
+
*/
|
|
1278
|
+
check(expectedMs = 0) {
|
|
1279
|
+
if (!(Number.isFinite(expectedMs) && expectedMs >= 0)) {
|
|
1280
|
+
throw new RangeError("expectedMs must be >= 0");
|
|
1281
|
+
}
|
|
1282
|
+
const elapsed = this.elapsedMs;
|
|
1283
|
+
if (elapsed + expectedMs > this.slaMs) {
|
|
1284
|
+
this.onBlock?.(elapsed, this.slaMs);
|
|
1285
|
+
throw new DeadlineExceeded(elapsed, this.slaMs);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
/** The soft near-deadline signal — symmetric to `BudgetGuard.advisory()`. */
|
|
1289
|
+
advisory() {
|
|
1290
|
+
const elapsed = this.elapsedMs;
|
|
1291
|
+
const usedBps = elapsed > 0 ? Math.min(1e4, Math.round(elapsed * 1e4 / this.slaMs)) : 0;
|
|
1292
|
+
return {
|
|
1293
|
+
nearDeadline: usedBps >= this.nearDeadlineBps,
|
|
1294
|
+
usedBps,
|
|
1295
|
+
remainingMs: Math.max(0, this.slaMs - elapsed),
|
|
1296
|
+
slaMs: this.slaMs,
|
|
1297
|
+
elapsedMs: elapsed
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
|
|
1130
1302
|
// src/middleware.ts
|
|
1131
1303
|
function usageTokens(modelId, usage) {
|
|
1132
1304
|
const u = usage;
|
|
@@ -1219,7 +1391,9 @@ function budgetGuardMiddleware(guard) {
|
|
|
1219
1391
|
export {
|
|
1220
1392
|
BudgetExceeded,
|
|
1221
1393
|
BudgetGuard,
|
|
1394
|
+
DeadlineExceeded,
|
|
1222
1395
|
FloeGuardError,
|
|
1396
|
+
LatencyBudget,
|
|
1223
1397
|
UnpriceableModelError,
|
|
1224
1398
|
budgetGuardMiddleware,
|
|
1225
1399
|
priceTokens,
|