floe-guard 0.4.0 → 0.6.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 +341 -105
- 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 +341 -105
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cost_map.json +216 -84
package/README.md
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
[](../LICENSE)
|
|
5
5
|
|
|
6
6
|
**A local budget guardrail for AI agents** — the TypeScript counterpart to the
|
|
7
|
-
[Python `floe-guard`](../README.md). It hard-stops your agent *before its next
|
|
8
|
-
call* when it would cross a USD spend ceiling
|
|
9
|
-
|
|
7
|
+
[Python `floe-guard`](../README.md). It hard-stops your agent *before its next
|
|
8
|
+
LLM or paid tool call* when it would cross a USD spend ceiling — tokens and
|
|
9
|
+
tool calls under one local ceiling. No account, no signup, no network. Runs in
|
|
10
|
+
your process.
|
|
10
11
|
|
|
11
12
|
Works with both **AI SDK v4 and v5** (`ai@4` / `ai@5`).
|
|
12
13
|
|
|
@@ -63,6 +64,21 @@ const adv = guard.advisory();
|
|
|
63
64
|
const model = adv.nearLimit ? openai("gpt-4o-mini") : openai("gpt-4o");
|
|
64
65
|
```
|
|
65
66
|
|
|
67
|
+
## Tool spend under the same ceiling
|
|
68
|
+
|
|
69
|
+
Paid tool calls (Apollo, Exa, scrapers) burn the same budget as tokens. The
|
|
70
|
+
full reserve/settle contract applies — and the price is known *before* the
|
|
71
|
+
call, so the pre-call hard-stop is exact:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const handle = guard.reserveTool(0.02); // throws BudgetExceeded BEFORE the call
|
|
75
|
+
const result = await apollo.peopleLookup(...);
|
|
76
|
+
guard.settleTool("apollo.people_lookup", 0.02, { reserved: handle });
|
|
77
|
+
|
|
78
|
+
guard.recordTool("exa.search", 0.004); // post-hoc, for metered APIs
|
|
79
|
+
guard.toolCosts; // { "apollo.people_lookup": 0.42, "exa.search": 0.11 }
|
|
80
|
+
```
|
|
81
|
+
|
|
66
82
|
## Per-call spend log
|
|
67
83
|
|
|
68
84
|
The guard keeps a typed, in-memory ledger of everything it priced: each
|
package/dist/index.cjs
CHANGED
|
@@ -304,6 +304,222 @@ var cost_map_default = {
|
|
|
304
304
|
litellm_provider: "openai",
|
|
305
305
|
mode: "chat"
|
|
306
306
|
},
|
|
307
|
+
"gemini-2.0-flash": {
|
|
308
|
+
input_cost_per_token: 1e-7,
|
|
309
|
+
output_cost_per_token: 4e-7,
|
|
310
|
+
litellm_provider: "gemini",
|
|
311
|
+
mode: "chat"
|
|
312
|
+
},
|
|
313
|
+
"gemini-2.0-flash-001": {
|
|
314
|
+
input_cost_per_token: 1e-7,
|
|
315
|
+
output_cost_per_token: 4e-7,
|
|
316
|
+
litellm_provider: "gemini",
|
|
317
|
+
mode: "chat"
|
|
318
|
+
},
|
|
319
|
+
"gemini-2.0-flash-lite": {
|
|
320
|
+
input_cost_per_token: 75e-9,
|
|
321
|
+
output_cost_per_token: 3e-7,
|
|
322
|
+
litellm_provider: "gemini",
|
|
323
|
+
mode: "chat"
|
|
324
|
+
},
|
|
325
|
+
"gemini-2.0-flash-lite-001": {
|
|
326
|
+
input_cost_per_token: 75e-9,
|
|
327
|
+
output_cost_per_token: 3e-7,
|
|
328
|
+
litellm_provider: "gemini",
|
|
329
|
+
mode: "chat"
|
|
330
|
+
},
|
|
331
|
+
"gemini-2.5-computer-use-preview-10-2025": {
|
|
332
|
+
input_cost_per_token: 125e-8,
|
|
333
|
+
output_cost_per_token: 1e-5,
|
|
334
|
+
litellm_provider: "gemini",
|
|
335
|
+
mode: "chat"
|
|
336
|
+
},
|
|
337
|
+
"gemini-2.5-flash": {
|
|
338
|
+
input_cost_per_token: 3e-7,
|
|
339
|
+
output_cost_per_token: 25e-7,
|
|
340
|
+
litellm_provider: "gemini",
|
|
341
|
+
mode: "chat"
|
|
342
|
+
},
|
|
343
|
+
"gemini-2.5-flash-lite": {
|
|
344
|
+
input_cost_per_token: 1e-7,
|
|
345
|
+
output_cost_per_token: 4e-7,
|
|
346
|
+
litellm_provider: "gemini",
|
|
347
|
+
mode: "chat"
|
|
348
|
+
},
|
|
349
|
+
"gemini-2.5-flash-lite-preview-06-17": {
|
|
350
|
+
input_cost_per_token: 1e-7,
|
|
351
|
+
output_cost_per_token: 4e-7,
|
|
352
|
+
litellm_provider: "gemini",
|
|
353
|
+
mode: "chat"
|
|
354
|
+
},
|
|
355
|
+
"gemini-2.5-flash-lite-preview-09-2025": {
|
|
356
|
+
input_cost_per_token: 1e-7,
|
|
357
|
+
output_cost_per_token: 4e-7,
|
|
358
|
+
litellm_provider: "gemini",
|
|
359
|
+
mode: "chat"
|
|
360
|
+
},
|
|
361
|
+
"gemini-2.5-flash-native-audio-latest": {
|
|
362
|
+
input_cost_per_token: 3e-7,
|
|
363
|
+
output_cost_per_token: 25e-7,
|
|
364
|
+
litellm_provider: "gemini",
|
|
365
|
+
mode: "chat"
|
|
366
|
+
},
|
|
367
|
+
"gemini-2.5-flash-native-audio-preview-09-2025": {
|
|
368
|
+
input_cost_per_token: 3e-7,
|
|
369
|
+
output_cost_per_token: 25e-7,
|
|
370
|
+
litellm_provider: "gemini",
|
|
371
|
+
mode: "chat"
|
|
372
|
+
},
|
|
373
|
+
"gemini-2.5-flash-native-audio-preview-12-2025": {
|
|
374
|
+
input_cost_per_token: 3e-7,
|
|
375
|
+
output_cost_per_token: 25e-7,
|
|
376
|
+
litellm_provider: "gemini",
|
|
377
|
+
mode: "chat"
|
|
378
|
+
},
|
|
379
|
+
"gemini-2.5-flash-preview-09-2025": {
|
|
380
|
+
input_cost_per_token: 3e-7,
|
|
381
|
+
output_cost_per_token: 25e-7,
|
|
382
|
+
litellm_provider: "gemini",
|
|
383
|
+
mode: "chat"
|
|
384
|
+
},
|
|
385
|
+
"gemini-2.5-pro": {
|
|
386
|
+
input_cost_per_token: 125e-8,
|
|
387
|
+
output_cost_per_token: 1e-5,
|
|
388
|
+
litellm_provider: "gemini",
|
|
389
|
+
mode: "chat"
|
|
390
|
+
},
|
|
391
|
+
"gemini-2.5-pro-preview-tts": {
|
|
392
|
+
input_cost_per_token: 125e-8,
|
|
393
|
+
output_cost_per_token: 1e-5,
|
|
394
|
+
litellm_provider: "gemini",
|
|
395
|
+
mode: "chat"
|
|
396
|
+
},
|
|
397
|
+
"gemini-3-flash-preview": {
|
|
398
|
+
input_cost_per_token: 5e-7,
|
|
399
|
+
output_cost_per_token: 3e-6,
|
|
400
|
+
litellm_provider: "gemini",
|
|
401
|
+
mode: "chat"
|
|
402
|
+
},
|
|
403
|
+
"gemini-3-pro-preview": {
|
|
404
|
+
input_cost_per_token: 2e-6,
|
|
405
|
+
output_cost_per_token: 12e-6,
|
|
406
|
+
litellm_provider: "gemini",
|
|
407
|
+
mode: "chat"
|
|
408
|
+
},
|
|
409
|
+
"gemini-3.1-flash-lite": {
|
|
410
|
+
input_cost_per_token: 25e-8,
|
|
411
|
+
output_cost_per_token: 15e-7,
|
|
412
|
+
litellm_provider: "gemini",
|
|
413
|
+
mode: "chat"
|
|
414
|
+
},
|
|
415
|
+
"gemini-3.1-flash-lite-preview": {
|
|
416
|
+
input_cost_per_token: 25e-8,
|
|
417
|
+
output_cost_per_token: 15e-7,
|
|
418
|
+
litellm_provider: "gemini",
|
|
419
|
+
mode: "chat"
|
|
420
|
+
},
|
|
421
|
+
"gemini-3.1-flash-live-preview": {
|
|
422
|
+
input_cost_per_token: 75e-8,
|
|
423
|
+
output_cost_per_token: 45e-7,
|
|
424
|
+
litellm_provider: "gemini",
|
|
425
|
+
mode: "chat"
|
|
426
|
+
},
|
|
427
|
+
"gemini-3.1-pro-preview": {
|
|
428
|
+
input_cost_per_token: 2e-6,
|
|
429
|
+
output_cost_per_token: 12e-6,
|
|
430
|
+
litellm_provider: "gemini",
|
|
431
|
+
mode: "chat"
|
|
432
|
+
},
|
|
433
|
+
"gemini-3.1-pro-preview-customtools": {
|
|
434
|
+
input_cost_per_token: 2e-6,
|
|
435
|
+
output_cost_per_token: 12e-6,
|
|
436
|
+
litellm_provider: "gemini",
|
|
437
|
+
mode: "chat"
|
|
438
|
+
},
|
|
439
|
+
"gemini-3.5-flash": {
|
|
440
|
+
input_cost_per_token: 15e-7,
|
|
441
|
+
output_cost_per_token: 9e-6,
|
|
442
|
+
litellm_provider: "gemini",
|
|
443
|
+
mode: "chat"
|
|
444
|
+
},
|
|
445
|
+
"gemini-3.5-flash-lite": {
|
|
446
|
+
input_cost_per_token: 3e-7,
|
|
447
|
+
output_cost_per_token: 25e-7,
|
|
448
|
+
litellm_provider: "gemini",
|
|
449
|
+
mode: "chat"
|
|
450
|
+
},
|
|
451
|
+
"gemini-3.6-flash": {
|
|
452
|
+
input_cost_per_token: 15e-7,
|
|
453
|
+
output_cost_per_token: 75e-7,
|
|
454
|
+
litellm_provider: "gemini",
|
|
455
|
+
mode: "chat"
|
|
456
|
+
},
|
|
457
|
+
"gemini-embedding-001": {
|
|
458
|
+
input_cost_per_token: 15e-8,
|
|
459
|
+
output_cost_per_token: 0,
|
|
460
|
+
litellm_provider: "gemini",
|
|
461
|
+
mode: "embedding"
|
|
462
|
+
},
|
|
463
|
+
"gemini-embedding-2": {
|
|
464
|
+
input_cost_per_token: 2e-7,
|
|
465
|
+
output_cost_per_token: 0,
|
|
466
|
+
litellm_provider: "gemini",
|
|
467
|
+
mode: "embedding"
|
|
468
|
+
},
|
|
469
|
+
"gemini-embedding-2-preview": {
|
|
470
|
+
input_cost_per_token: 2e-7,
|
|
471
|
+
output_cost_per_token: 0,
|
|
472
|
+
litellm_provider: "gemini",
|
|
473
|
+
mode: "embedding"
|
|
474
|
+
},
|
|
475
|
+
"gemini-exp-1206": {
|
|
476
|
+
input_cost_per_token: 3e-7,
|
|
477
|
+
output_cost_per_token: 25e-7,
|
|
478
|
+
litellm_provider: "gemini",
|
|
479
|
+
mode: "chat"
|
|
480
|
+
},
|
|
481
|
+
"gemini-flash-latest": {
|
|
482
|
+
input_cost_per_token: 3e-7,
|
|
483
|
+
output_cost_per_token: 25e-7,
|
|
484
|
+
litellm_provider: "gemini",
|
|
485
|
+
mode: "chat"
|
|
486
|
+
},
|
|
487
|
+
"gemini-flash-lite-latest": {
|
|
488
|
+
input_cost_per_token: 1e-7,
|
|
489
|
+
output_cost_per_token: 4e-7,
|
|
490
|
+
litellm_provider: "gemini",
|
|
491
|
+
mode: "chat"
|
|
492
|
+
},
|
|
493
|
+
"gemini-gemma-2-27b-it": {
|
|
494
|
+
input_cost_per_token: 35e-8,
|
|
495
|
+
output_cost_per_token: 105e-8,
|
|
496
|
+
litellm_provider: "gemini",
|
|
497
|
+
mode: "chat"
|
|
498
|
+
},
|
|
499
|
+
"gemini-gemma-2-9b-it": {
|
|
500
|
+
input_cost_per_token: 35e-8,
|
|
501
|
+
output_cost_per_token: 105e-8,
|
|
502
|
+
litellm_provider: "gemini",
|
|
503
|
+
mode: "chat"
|
|
504
|
+
},
|
|
505
|
+
"gemini-omni-flash-preview": {
|
|
506
|
+
input_cost_per_token: 15e-7,
|
|
507
|
+
output_cost_per_token: 9e-6,
|
|
508
|
+
litellm_provider: "gemini",
|
|
509
|
+
mode: "chat"
|
|
510
|
+
},
|
|
511
|
+
"gemini-pro-latest": {
|
|
512
|
+
input_cost_per_token: 125e-8,
|
|
513
|
+
output_cost_per_token: 1e-5,
|
|
514
|
+
litellm_provider: "gemini",
|
|
515
|
+
mode: "chat"
|
|
516
|
+
},
|
|
517
|
+
"gemini-robotics-er-1.5-preview": {
|
|
518
|
+
input_cost_per_token: 3e-7,
|
|
519
|
+
output_cost_per_token: 25e-7,
|
|
520
|
+
litellm_provider: "gemini",
|
|
521
|
+
mode: "chat"
|
|
522
|
+
},
|
|
307
523
|
"gpt-3.5-turbo": {
|
|
308
524
|
input_cost_per_token: 5e-7,
|
|
309
525
|
output_cost_per_token: 15e-7,
|
|
@@ -478,18 +694,6 @@ var cost_map_default = {
|
|
|
478
694
|
litellm_provider: "openai",
|
|
479
695
|
mode: "chat"
|
|
480
696
|
},
|
|
481
|
-
"gpt-4o-mini-realtime-preview": {
|
|
482
|
-
input_cost_per_token: 6e-7,
|
|
483
|
-
output_cost_per_token: 24e-7,
|
|
484
|
-
litellm_provider: "openai",
|
|
485
|
-
mode: "chat"
|
|
486
|
-
},
|
|
487
|
-
"gpt-4o-mini-realtime-preview-2024-12-17": {
|
|
488
|
-
input_cost_per_token: 6e-7,
|
|
489
|
-
output_cost_per_token: 24e-7,
|
|
490
|
-
litellm_provider: "openai",
|
|
491
|
-
mode: "chat"
|
|
492
|
-
},
|
|
493
697
|
"gpt-4o-mini-search-preview": {
|
|
494
698
|
input_cost_per_token: 15e-8,
|
|
495
699
|
output_cost_per_token: 6e-7,
|
|
@@ -502,24 +706,6 @@ var cost_map_default = {
|
|
|
502
706
|
litellm_provider: "openai",
|
|
503
707
|
mode: "chat"
|
|
504
708
|
},
|
|
505
|
-
"gpt-4o-realtime-preview": {
|
|
506
|
-
input_cost_per_token: 5e-6,
|
|
507
|
-
output_cost_per_token: 2e-5,
|
|
508
|
-
litellm_provider: "openai",
|
|
509
|
-
mode: "chat"
|
|
510
|
-
},
|
|
511
|
-
"gpt-4o-realtime-preview-2024-12-17": {
|
|
512
|
-
input_cost_per_token: 5e-6,
|
|
513
|
-
output_cost_per_token: 2e-5,
|
|
514
|
-
litellm_provider: "openai",
|
|
515
|
-
mode: "chat"
|
|
516
|
-
},
|
|
517
|
-
"gpt-4o-realtime-preview-2025-06-03": {
|
|
518
|
-
input_cost_per_token: 5e-6,
|
|
519
|
-
output_cost_per_token: 2e-5,
|
|
520
|
-
litellm_provider: "openai",
|
|
521
|
-
mode: "chat"
|
|
522
|
-
},
|
|
523
709
|
"gpt-4o-search-preview": {
|
|
524
710
|
input_cost_per_token: 25e-7,
|
|
525
711
|
output_cost_per_token: 1e-5,
|
|
@@ -742,60 +928,6 @@ var cost_map_default = {
|
|
|
742
928
|
litellm_provider: "openai",
|
|
743
929
|
mode: "chat"
|
|
744
930
|
},
|
|
745
|
-
"gpt-realtime": {
|
|
746
|
-
input_cost_per_token: 4e-6,
|
|
747
|
-
output_cost_per_token: 16e-6,
|
|
748
|
-
litellm_provider: "openai",
|
|
749
|
-
mode: "chat"
|
|
750
|
-
},
|
|
751
|
-
"gpt-realtime-1.5": {
|
|
752
|
-
input_cost_per_token: 4e-6,
|
|
753
|
-
output_cost_per_token: 16e-6,
|
|
754
|
-
litellm_provider: "openai",
|
|
755
|
-
mode: "chat"
|
|
756
|
-
},
|
|
757
|
-
"gpt-realtime-2": {
|
|
758
|
-
input_cost_per_token: 4e-6,
|
|
759
|
-
output_cost_per_token: 16e-6,
|
|
760
|
-
litellm_provider: "openai",
|
|
761
|
-
mode: "chat"
|
|
762
|
-
},
|
|
763
|
-
"gpt-realtime-2.1": {
|
|
764
|
-
input_cost_per_token: 4e-6,
|
|
765
|
-
output_cost_per_token: 24e-6,
|
|
766
|
-
litellm_provider: "openai",
|
|
767
|
-
mode: "chat"
|
|
768
|
-
},
|
|
769
|
-
"gpt-realtime-2.1-mini": {
|
|
770
|
-
input_cost_per_token: 6e-7,
|
|
771
|
-
output_cost_per_token: 24e-7,
|
|
772
|
-
litellm_provider: "openai",
|
|
773
|
-
mode: "chat"
|
|
774
|
-
},
|
|
775
|
-
"gpt-realtime-2025-08-28": {
|
|
776
|
-
input_cost_per_token: 4e-6,
|
|
777
|
-
output_cost_per_token: 16e-6,
|
|
778
|
-
litellm_provider: "openai",
|
|
779
|
-
mode: "chat"
|
|
780
|
-
},
|
|
781
|
-
"gpt-realtime-mini": {
|
|
782
|
-
input_cost_per_token: 6e-7,
|
|
783
|
-
output_cost_per_token: 24e-7,
|
|
784
|
-
litellm_provider: "openai",
|
|
785
|
-
mode: "chat"
|
|
786
|
-
},
|
|
787
|
-
"gpt-realtime-mini-2025-10-06": {
|
|
788
|
-
input_cost_per_token: 6e-7,
|
|
789
|
-
output_cost_per_token: 24e-7,
|
|
790
|
-
litellm_provider: "openai",
|
|
791
|
-
mode: "chat"
|
|
792
|
-
},
|
|
793
|
-
"gpt-realtime-mini-2025-12-15": {
|
|
794
|
-
input_cost_per_token: 6e-7,
|
|
795
|
-
output_cost_per_token: 24e-7,
|
|
796
|
-
litellm_provider: "openai",
|
|
797
|
-
mode: "chat"
|
|
798
|
-
},
|
|
799
931
|
"llama-3.1-8b-instant": {
|
|
800
932
|
input_cost_per_token: 5e-8,
|
|
801
933
|
output_cost_per_token: 8e-8,
|
|
@@ -992,13 +1124,26 @@ var BudgetGuard = class {
|
|
|
992
1124
|
failClosed;
|
|
993
1125
|
nearLimitBps;
|
|
994
1126
|
onBlock;
|
|
995
|
-
/**
|
|
996
|
-
|
|
1127
|
+
/**
|
|
1128
|
+
* Costs of the most recent priced LLM call and tool call, tracked
|
|
1129
|
+
* SEPARATELY: the default next-call prediction is the max of the two, so a
|
|
1130
|
+
* cheap tool call can't shrink the estimate right before an expensive LLM
|
|
1131
|
+
* call (or vice versa) — conservative beats one-call-too-late.
|
|
1132
|
+
*/
|
|
1133
|
+
lastLlmCost = 0;
|
|
1134
|
+
lastToolCost = 0;
|
|
997
1135
|
/** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */
|
|
998
1136
|
reserved = 0;
|
|
999
1137
|
/** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */
|
|
1000
1138
|
spendEvents = [];
|
|
1001
1139
|
maxLogEvents;
|
|
1140
|
+
/**
|
|
1141
|
+
* Per-tool running totals (settleTool/recordTool) — the tool side of the one
|
|
1142
|
+
* shared ceiling, exposed via the toolCosts getter. null-prototype: tool
|
|
1143
|
+
* names are caller-supplied strings, so a "__proto__" name is stored as
|
|
1144
|
+
* plain data instead of mutating the object's prototype.
|
|
1145
|
+
*/
|
|
1146
|
+
toolCostTotals = /* @__PURE__ */ Object.create(null);
|
|
1002
1147
|
/**
|
|
1003
1148
|
* @param limitUsd the spend ceiling, in USD. `0` blocks the very first call.
|
|
1004
1149
|
*/
|
|
@@ -1030,7 +1175,8 @@ var BudgetGuard = class {
|
|
|
1030
1175
|
* Throw {@link BudgetExceeded} if the next call would cross the ceiling.
|
|
1031
1176
|
*
|
|
1032
1177
|
* Call this immediately before each LLM request. The "next call" is estimated
|
|
1033
|
-
*
|
|
1178
|
+
* conservatively as the costlier of the last LLM call and the last tool call
|
|
1179
|
+
* (override with `estimatedNextCost`); the
|
|
1034
1180
|
* first call is always allowed unless the ceiling is already met. In-flight
|
|
1035
1181
|
* reservations count toward the total, so this stays correct alongside
|
|
1036
1182
|
* {@link BudgetGuard.reserve}.
|
|
@@ -1039,7 +1185,7 @@ var BudgetGuard = class {
|
|
|
1039
1185
|
* `settle()`, which hold the estimate across the await.
|
|
1040
1186
|
*/
|
|
1041
1187
|
check(estimatedNextCost) {
|
|
1042
|
-
const rawEstimate = estimatedNextCost === void 0 ? this.
|
|
1188
|
+
const rawEstimate = estimatedNextCost === void 0 ? this.defaultEstimate() : estimatedNextCost;
|
|
1043
1189
|
if (!Number.isFinite(rawEstimate)) {
|
|
1044
1190
|
throw new RangeError(
|
|
1045
1191
|
`estimatedNextCost must be a finite number, got ${rawEstimate}`
|
|
@@ -1060,10 +1206,11 @@ var BudgetGuard = class {
|
|
|
1060
1206
|
* the same stale total. Throws {@link BudgetExceeded} (without reserving) if
|
|
1061
1207
|
* the reservation would cross the ceiling. Returns the reservation handle to
|
|
1062
1208
|
* pass to {@link BudgetGuard.settle} (or {@link BudgetGuard.release} on error).
|
|
1063
|
-
* `estimatedCost` defaults to the last call
|
|
1209
|
+
* `estimatedCost` defaults to the costlier of the last LLM call and the last
|
|
1210
|
+
* tool call.
|
|
1064
1211
|
*/
|
|
1065
1212
|
reserve(estimatedCost) {
|
|
1066
|
-
const rawEstimate = estimatedCost === void 0 ? this.
|
|
1213
|
+
const rawEstimate = estimatedCost === void 0 ? this.defaultEstimate() : estimatedCost;
|
|
1067
1214
|
if (!Number.isFinite(rawEstimate)) {
|
|
1068
1215
|
throw new RangeError(
|
|
1069
1216
|
`estimatedCost must be a finite number, got ${rawEstimate}`
|
|
@@ -1115,13 +1262,13 @@ var BudgetGuard = class {
|
|
|
1115
1262
|
throw err;
|
|
1116
1263
|
}
|
|
1117
1264
|
if (reserved) {
|
|
1118
|
-
this.
|
|
1265
|
+
this.consumeReservation(reserved);
|
|
1119
1266
|
}
|
|
1120
1267
|
this.spentUsd += cost;
|
|
1121
1268
|
if (this.spentUsd - this.limitUsd > 0 && this.spentUsd - this.limitUsd < EPS) {
|
|
1122
1269
|
this.spentUsd = this.limitUsd;
|
|
1123
1270
|
}
|
|
1124
|
-
this.
|
|
1271
|
+
this.lastLlmCost = cost;
|
|
1125
1272
|
this.appendEvent({
|
|
1126
1273
|
timestamp: Date.now() / 1e3,
|
|
1127
1274
|
kind: "llm",
|
|
@@ -1151,24 +1298,64 @@ var BudgetGuard = class {
|
|
|
1151
1298
|
});
|
|
1152
1299
|
}
|
|
1153
1300
|
/**
|
|
1154
|
-
*
|
|
1301
|
+
* Atomically check the ceiling AND hold a tool call's cost in flight.
|
|
1155
1302
|
*
|
|
1156
|
-
*
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
1160
|
-
*
|
|
1161
|
-
*
|
|
1162
|
-
*
|
|
1303
|
+
* The tool-spend counterpart of {@link BudgetGuard.reserve} — and STRONGER
|
|
1304
|
+
* than the LLM path, because a paid tool's price is usually known exactly
|
|
1305
|
+
* before the call, so the pre-call hard-stop is precise rather than
|
|
1306
|
+
* estimated:
|
|
1307
|
+
*
|
|
1308
|
+
* const handle = guard.reserveTool(0.02); // throws BEFORE Apollo runs
|
|
1309
|
+
* const result = await apollo.peopleLookup(...);
|
|
1310
|
+
* guard.settleTool("apollo.people_lookup", 0.02, { reserved: handle });
|
|
1311
|
+
*
|
|
1312
|
+
* Throws {@link BudgetExceeded} (without reserving) if the call would cross
|
|
1313
|
+
* the ceiling. The estimate is required — tools have no last-cost prediction
|
|
1314
|
+
* worth falling back to. Pass the returned handle to
|
|
1315
|
+
* {@link BudgetGuard.settleTool}, or {@link BudgetGuard.release} on failure.
|
|
1163
1316
|
*/
|
|
1164
|
-
|
|
1317
|
+
reserveTool(estimatedCost) {
|
|
1318
|
+
if (estimatedCost === void 0) {
|
|
1319
|
+
throw new RangeError("reserveTool requires an estimated cost, got undefined");
|
|
1320
|
+
}
|
|
1321
|
+
if (!Number.isFinite(estimatedCost) || estimatedCost < 0) {
|
|
1322
|
+
throw new RangeError(
|
|
1323
|
+
`estimatedCost must be a finite, non-negative number, got ${estimatedCost}`
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
return this.reserve(estimatedCost);
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Release a reservation and record a tool call's actual cost.
|
|
1330
|
+
*
|
|
1331
|
+
* `recordTool` is `settleTool` with no reservation. The caller supplies the
|
|
1332
|
+
* cost — tools have no token usage to price. Accrues into the same
|
|
1333
|
+
* `spentUsd` ceiling as tokens, tallies the per-tool total
|
|
1334
|
+
* ({@link BudgetGuard.toolCosts}), updates the tool side of the next-call
|
|
1335
|
+
* estimate (tracked separately from the LLM side; the default prediction is
|
|
1336
|
+
* the max of the two, so a tool-hammering loop's plain `check()` stops
|
|
1337
|
+
* BEFORE the crossing call without a cheap tool shrinking the LLM
|
|
1338
|
+
* prediction), and appends
|
|
1339
|
+
* a `kind: "tool"` {@link SpendEvent} to {@link BudgetGuard.spendLog}.
|
|
1340
|
+
* Returns `costUsd`.
|
|
1341
|
+
*/
|
|
1342
|
+
settleTool(tool, costUsd, options = {}) {
|
|
1165
1343
|
if (!Number.isFinite(costUsd) || costUsd < 0) {
|
|
1166
1344
|
throw new RangeError(`costUsd must be a finite, non-negative number, got ${costUsd}`);
|
|
1167
1345
|
}
|
|
1346
|
+
const reserved = options.reserved ?? 0;
|
|
1347
|
+
if (!Number.isFinite(reserved) || reserved < 0) {
|
|
1348
|
+
throw new RangeError(`reserved must be a finite, non-negative number, got ${reserved}`);
|
|
1349
|
+
}
|
|
1350
|
+
if (reserved) {
|
|
1351
|
+
this.consumeReservation(reserved);
|
|
1352
|
+
}
|
|
1168
1353
|
this.spentUsd += costUsd;
|
|
1169
1354
|
if (this.spentUsd - this.limitUsd > 0 && this.spentUsd - this.limitUsd < EPS) {
|
|
1170
1355
|
this.spentUsd = this.limitUsd;
|
|
1171
1356
|
}
|
|
1357
|
+
this.lastToolCost = costUsd;
|
|
1358
|
+
this.toolCostTotals[tool] = (this.toolCostTotals[tool] ?? 0) + costUsd;
|
|
1172
1359
|
this.appendEvent({
|
|
1173
1360
|
timestamp: Date.now() / 1e3,
|
|
1174
1361
|
kind: "tool",
|
|
@@ -1176,10 +1363,22 @@ var BudgetGuard = class {
|
|
|
1176
1363
|
promptTokens: null,
|
|
1177
1364
|
completionTokens: null,
|
|
1178
1365
|
costUsd,
|
|
1179
|
-
...options.label !== void 0 ? { label: options.label } : {}
|
|
1366
|
+
...options.label !== void 0 ? { label: options.label } : {},
|
|
1367
|
+
...reserved ? { reserved } : {}
|
|
1180
1368
|
});
|
|
1181
1369
|
return costUsd;
|
|
1182
1370
|
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.
|
|
1373
|
+
*
|
|
1374
|
+
* Post-hoc accrual for costs only known after the call (metered APIs); when
|
|
1375
|
+
* the price is known up front, {@link BudgetGuard.reserveTool} /
|
|
1376
|
+
* {@link BudgetGuard.settleTool} give the stronger pre-call hard-stop. See
|
|
1377
|
+
* `settleTool` for the full contract. Returns `costUsd`.
|
|
1378
|
+
*/
|
|
1379
|
+
recordTool(tool, costUsd, options = {}) {
|
|
1380
|
+
return this.settleTool(tool, costUsd, { reserved: 0, label: options.label });
|
|
1381
|
+
}
|
|
1183
1382
|
/**
|
|
1184
1383
|
* Drop an in-flight reservation without recording spend (e.g. the call failed
|
|
1185
1384
|
* before producing usage). Safe to call with `0`.
|
|
@@ -1189,16 +1388,25 @@ var BudgetGuard = class {
|
|
|
1189
1388
|
throw new RangeError(`reserved must be a finite, non-negative number, got ${reserved}`);
|
|
1190
1389
|
}
|
|
1191
1390
|
if (!reserved) return;
|
|
1192
|
-
this.
|
|
1391
|
+
this.consumeReservation(reserved);
|
|
1193
1392
|
}
|
|
1194
1393
|
/** USD left before the ceiling, net of in-flight reservations (never negative). */
|
|
1195
1394
|
get remainingUsd() {
|
|
1196
1395
|
return Math.max(0, this.limitUsd - this.spentUsd - this.reserved);
|
|
1197
1396
|
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Per-tool running USD totals, keyed by the name given to `settleTool()` /
|
|
1399
|
+
* `recordTool()` — e.g. `{"apollo.people_lookup": 0.42, "exa.search": 0.11}`.
|
|
1400
|
+
* Makes the token/tool split of the one shared ceiling inspectable
|
|
1401
|
+
* (`spentUsd - sum of toolCosts` is the token side). Returns a snapshot copy.
|
|
1402
|
+
*/
|
|
1403
|
+
get toolCosts() {
|
|
1404
|
+
return { ...this.toolCostTotals };
|
|
1405
|
+
}
|
|
1198
1406
|
/**
|
|
1199
1407
|
* The per-call spend ledger, oldest first — one {@link SpendEvent} per priced
|
|
1200
|
-
* `record()` / `settle()` / `recordTool()`. Returns a
|
|
1201
|
-
* it cannot corrupt the ledger.
|
|
1408
|
+
* `record()` / `settle()` / `recordTool()` / `settleTool()`. Returns a
|
|
1409
|
+
* snapshot copy: mutating it cannot corrupt the ledger.
|
|
1202
1410
|
*/
|
|
1203
1411
|
get spendLog() {
|
|
1204
1412
|
return [...this.spendEvents];
|
|
@@ -1229,6 +1437,34 @@ var BudgetGuard = class {
|
|
|
1229
1437
|
`;
|
|
1230
1438
|
}).join("");
|
|
1231
1439
|
}
|
|
1440
|
+
/**
|
|
1441
|
+
* The default next-call prediction when the caller supplies no estimate.
|
|
1442
|
+
* Conservative: the costlier of the last LLM call and the last tool call — a
|
|
1443
|
+
* mixed loop predicts the pricier kind, which at worst blocks one call early
|
|
1444
|
+
* (fail-closed) rather than letting a crossing call through because the LAST
|
|
1445
|
+
* event happened to be cheap.
|
|
1446
|
+
*/
|
|
1447
|
+
defaultEstimate() {
|
|
1448
|
+
return Math.max(this.lastLlmCost, this.lastToolCost);
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Subtract a settled/released hold from the in-flight tally. A handle larger
|
|
1452
|
+
* than EVERYTHING currently held cannot have come from a matching
|
|
1453
|
+
* `reserve()` — throwing beats silently clamping, which would free OTHER
|
|
1454
|
+
* callers' holds and fail the ceiling open. The epsilon absorbs float dust
|
|
1455
|
+
* from accumulating and draining many holds; per-caller over-release (a
|
|
1456
|
+
* handle within the total but larger than the caller's own hold) is
|
|
1457
|
+
* undetectable without per-handle tracking and remains the caller's
|
|
1458
|
+
* responsibility.
|
|
1459
|
+
*/
|
|
1460
|
+
consumeReservation(reserved) {
|
|
1461
|
+
if (reserved > this.reserved + EPS) {
|
|
1462
|
+
throw new RangeError(
|
|
1463
|
+
`reserved handle (${reserved}) exceeds total in-flight reservations (${this.reserved}) \u2014 a handle must come from a matching reserve()`
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
this.reserved = Math.max(0, this.reserved - reserved);
|
|
1467
|
+
}
|
|
1232
1468
|
appendEvent(event) {
|
|
1233
1469
|
this.spendEvents.push(Object.freeze(event));
|
|
1234
1470
|
if (this.maxLogEvents !== void 0 && this.spendEvents.length > this.maxLogEvents) {
|