floe-guard 0.3.0 → 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/dist/index.cjs +81 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -1
- package/dist/index.d.ts +77 -1
- package/dist/index.js +79 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -258,6 +258,77 @@ declare class BudgetGuard {
|
|
|
258
258
|
advisory(): BudgetAdvisory;
|
|
259
259
|
}
|
|
260
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
|
+
|
|
261
332
|
/**
|
|
262
333
|
* Exceptions for floe-guard.
|
|
263
334
|
*
|
|
@@ -293,6 +364,11 @@ declare class UnpriceableModelError extends FloeGuardError {
|
|
|
293
364
|
readonly model: string;
|
|
294
365
|
constructor(model: string);
|
|
295
366
|
}
|
|
367
|
+
declare class DeadlineExceeded extends FloeGuardError {
|
|
368
|
+
readonly elapsedMs: number;
|
|
369
|
+
readonly slaMs: number;
|
|
370
|
+
constructor(elapsedMs: number, slaMs: number);
|
|
371
|
+
}
|
|
296
372
|
|
|
297
373
|
/**
|
|
298
374
|
* Vercel AI SDK middleware that enforces a {@link BudgetGuard} in the call path.
|
|
@@ -363,4 +439,4 @@ interface BudgetGuardMiddleware {
|
|
|
363
439
|
*/
|
|
364
440
|
declare function budgetGuardMiddleware(guard: BudgetGuard): BudgetGuardMiddleware;
|
|
365
441
|
|
|
366
|
-
export { type BudgetAdvisory, BudgetExceeded, BudgetGuard, type BudgetGuardMiddleware, type BudgetGuardOptions, FloeGuardError, type ManualPrice, type PricedModel, type SpendEvent, 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
|
@@ -258,6 +258,77 @@ declare class BudgetGuard {
|
|
|
258
258
|
advisory(): BudgetAdvisory;
|
|
259
259
|
}
|
|
260
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
|
+
|
|
261
332
|
/**
|
|
262
333
|
* Exceptions for floe-guard.
|
|
263
334
|
*
|
|
@@ -293,6 +364,11 @@ declare class UnpriceableModelError extends FloeGuardError {
|
|
|
293
364
|
readonly model: string;
|
|
294
365
|
constructor(model: string);
|
|
295
366
|
}
|
|
367
|
+
declare class DeadlineExceeded extends FloeGuardError {
|
|
368
|
+
readonly elapsedMs: number;
|
|
369
|
+
readonly slaMs: number;
|
|
370
|
+
constructor(elapsedMs: number, slaMs: number);
|
|
371
|
+
}
|
|
296
372
|
|
|
297
373
|
/**
|
|
298
374
|
* Vercel AI SDK middleware that enforces a {@link BudgetGuard} in the call path.
|
|
@@ -363,4 +439,4 @@ interface BudgetGuardMiddleware {
|
|
|
363
439
|
*/
|
|
364
440
|
declare function budgetGuardMiddleware(guard: BudgetGuard): BudgetGuardMiddleware;
|
|
365
441
|
|
|
366
|
-
export { type BudgetAdvisory, BudgetExceeded, BudgetGuard, type BudgetGuardMiddleware, type BudgetGuardOptions, FloeGuardError, type ManualPrice, type PricedModel, type SpendEvent, 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 = {};
|
|
@@ -1222,6 +1237,68 @@ function defaultOnBlock(spentUsd, limitUsd) {
|
|
|
1222
1237
|
);
|
|
1223
1238
|
}
|
|
1224
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
|
+
|
|
1225
1302
|
// src/middleware.ts
|
|
1226
1303
|
function usageTokens(modelId, usage) {
|
|
1227
1304
|
const u = usage;
|
|
@@ -1314,7 +1391,9 @@ function budgetGuardMiddleware(guard) {
|
|
|
1314
1391
|
export {
|
|
1315
1392
|
BudgetExceeded,
|
|
1316
1393
|
BudgetGuard,
|
|
1394
|
+
DeadlineExceeded,
|
|
1317
1395
|
FloeGuardError,
|
|
1396
|
+
LatencyBudget,
|
|
1318
1397
|
UnpriceableModelError,
|
|
1319
1398
|
budgetGuardMiddleware,
|
|
1320
1399
|
priceTokens,
|