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/dist/index.js CHANGED
@@ -275,6 +275,222 @@ var cost_map_default = {
275
275
  litellm_provider: "openai",
276
276
  mode: "chat"
277
277
  },
278
+ "gemini-2.0-flash": {
279
+ input_cost_per_token: 1e-7,
280
+ output_cost_per_token: 4e-7,
281
+ litellm_provider: "gemini",
282
+ mode: "chat"
283
+ },
284
+ "gemini-2.0-flash-001": {
285
+ input_cost_per_token: 1e-7,
286
+ output_cost_per_token: 4e-7,
287
+ litellm_provider: "gemini",
288
+ mode: "chat"
289
+ },
290
+ "gemini-2.0-flash-lite": {
291
+ input_cost_per_token: 75e-9,
292
+ output_cost_per_token: 3e-7,
293
+ litellm_provider: "gemini",
294
+ mode: "chat"
295
+ },
296
+ "gemini-2.0-flash-lite-001": {
297
+ input_cost_per_token: 75e-9,
298
+ output_cost_per_token: 3e-7,
299
+ litellm_provider: "gemini",
300
+ mode: "chat"
301
+ },
302
+ "gemini-2.5-computer-use-preview-10-2025": {
303
+ input_cost_per_token: 125e-8,
304
+ output_cost_per_token: 1e-5,
305
+ litellm_provider: "gemini",
306
+ mode: "chat"
307
+ },
308
+ "gemini-2.5-flash": {
309
+ input_cost_per_token: 3e-7,
310
+ output_cost_per_token: 25e-7,
311
+ litellm_provider: "gemini",
312
+ mode: "chat"
313
+ },
314
+ "gemini-2.5-flash-lite": {
315
+ input_cost_per_token: 1e-7,
316
+ output_cost_per_token: 4e-7,
317
+ litellm_provider: "gemini",
318
+ mode: "chat"
319
+ },
320
+ "gemini-2.5-flash-lite-preview-06-17": {
321
+ input_cost_per_token: 1e-7,
322
+ output_cost_per_token: 4e-7,
323
+ litellm_provider: "gemini",
324
+ mode: "chat"
325
+ },
326
+ "gemini-2.5-flash-lite-preview-09-2025": {
327
+ input_cost_per_token: 1e-7,
328
+ output_cost_per_token: 4e-7,
329
+ litellm_provider: "gemini",
330
+ mode: "chat"
331
+ },
332
+ "gemini-2.5-flash-native-audio-latest": {
333
+ input_cost_per_token: 3e-7,
334
+ output_cost_per_token: 25e-7,
335
+ litellm_provider: "gemini",
336
+ mode: "chat"
337
+ },
338
+ "gemini-2.5-flash-native-audio-preview-09-2025": {
339
+ input_cost_per_token: 3e-7,
340
+ output_cost_per_token: 25e-7,
341
+ litellm_provider: "gemini",
342
+ mode: "chat"
343
+ },
344
+ "gemini-2.5-flash-native-audio-preview-12-2025": {
345
+ input_cost_per_token: 3e-7,
346
+ output_cost_per_token: 25e-7,
347
+ litellm_provider: "gemini",
348
+ mode: "chat"
349
+ },
350
+ "gemini-2.5-flash-preview-09-2025": {
351
+ input_cost_per_token: 3e-7,
352
+ output_cost_per_token: 25e-7,
353
+ litellm_provider: "gemini",
354
+ mode: "chat"
355
+ },
356
+ "gemini-2.5-pro": {
357
+ input_cost_per_token: 125e-8,
358
+ output_cost_per_token: 1e-5,
359
+ litellm_provider: "gemini",
360
+ mode: "chat"
361
+ },
362
+ "gemini-2.5-pro-preview-tts": {
363
+ input_cost_per_token: 125e-8,
364
+ output_cost_per_token: 1e-5,
365
+ litellm_provider: "gemini",
366
+ mode: "chat"
367
+ },
368
+ "gemini-3-flash-preview": {
369
+ input_cost_per_token: 5e-7,
370
+ output_cost_per_token: 3e-6,
371
+ litellm_provider: "gemini",
372
+ mode: "chat"
373
+ },
374
+ "gemini-3-pro-preview": {
375
+ input_cost_per_token: 2e-6,
376
+ output_cost_per_token: 12e-6,
377
+ litellm_provider: "gemini",
378
+ mode: "chat"
379
+ },
380
+ "gemini-3.1-flash-lite": {
381
+ input_cost_per_token: 25e-8,
382
+ output_cost_per_token: 15e-7,
383
+ litellm_provider: "gemini",
384
+ mode: "chat"
385
+ },
386
+ "gemini-3.1-flash-lite-preview": {
387
+ input_cost_per_token: 25e-8,
388
+ output_cost_per_token: 15e-7,
389
+ litellm_provider: "gemini",
390
+ mode: "chat"
391
+ },
392
+ "gemini-3.1-flash-live-preview": {
393
+ input_cost_per_token: 75e-8,
394
+ output_cost_per_token: 45e-7,
395
+ litellm_provider: "gemini",
396
+ mode: "chat"
397
+ },
398
+ "gemini-3.1-pro-preview": {
399
+ input_cost_per_token: 2e-6,
400
+ output_cost_per_token: 12e-6,
401
+ litellm_provider: "gemini",
402
+ mode: "chat"
403
+ },
404
+ "gemini-3.1-pro-preview-customtools": {
405
+ input_cost_per_token: 2e-6,
406
+ output_cost_per_token: 12e-6,
407
+ litellm_provider: "gemini",
408
+ mode: "chat"
409
+ },
410
+ "gemini-3.5-flash": {
411
+ input_cost_per_token: 15e-7,
412
+ output_cost_per_token: 9e-6,
413
+ litellm_provider: "gemini",
414
+ mode: "chat"
415
+ },
416
+ "gemini-3.5-flash-lite": {
417
+ input_cost_per_token: 3e-7,
418
+ output_cost_per_token: 25e-7,
419
+ litellm_provider: "gemini",
420
+ mode: "chat"
421
+ },
422
+ "gemini-3.6-flash": {
423
+ input_cost_per_token: 15e-7,
424
+ output_cost_per_token: 75e-7,
425
+ litellm_provider: "gemini",
426
+ mode: "chat"
427
+ },
428
+ "gemini-embedding-001": {
429
+ input_cost_per_token: 15e-8,
430
+ output_cost_per_token: 0,
431
+ litellm_provider: "gemini",
432
+ mode: "embedding"
433
+ },
434
+ "gemini-embedding-2": {
435
+ input_cost_per_token: 2e-7,
436
+ output_cost_per_token: 0,
437
+ litellm_provider: "gemini",
438
+ mode: "embedding"
439
+ },
440
+ "gemini-embedding-2-preview": {
441
+ input_cost_per_token: 2e-7,
442
+ output_cost_per_token: 0,
443
+ litellm_provider: "gemini",
444
+ mode: "embedding"
445
+ },
446
+ "gemini-exp-1206": {
447
+ input_cost_per_token: 3e-7,
448
+ output_cost_per_token: 25e-7,
449
+ litellm_provider: "gemini",
450
+ mode: "chat"
451
+ },
452
+ "gemini-flash-latest": {
453
+ input_cost_per_token: 3e-7,
454
+ output_cost_per_token: 25e-7,
455
+ litellm_provider: "gemini",
456
+ mode: "chat"
457
+ },
458
+ "gemini-flash-lite-latest": {
459
+ input_cost_per_token: 1e-7,
460
+ output_cost_per_token: 4e-7,
461
+ litellm_provider: "gemini",
462
+ mode: "chat"
463
+ },
464
+ "gemini-gemma-2-27b-it": {
465
+ input_cost_per_token: 35e-8,
466
+ output_cost_per_token: 105e-8,
467
+ litellm_provider: "gemini",
468
+ mode: "chat"
469
+ },
470
+ "gemini-gemma-2-9b-it": {
471
+ input_cost_per_token: 35e-8,
472
+ output_cost_per_token: 105e-8,
473
+ litellm_provider: "gemini",
474
+ mode: "chat"
475
+ },
476
+ "gemini-omni-flash-preview": {
477
+ input_cost_per_token: 15e-7,
478
+ output_cost_per_token: 9e-6,
479
+ litellm_provider: "gemini",
480
+ mode: "chat"
481
+ },
482
+ "gemini-pro-latest": {
483
+ input_cost_per_token: 125e-8,
484
+ output_cost_per_token: 1e-5,
485
+ litellm_provider: "gemini",
486
+ mode: "chat"
487
+ },
488
+ "gemini-robotics-er-1.5-preview": {
489
+ input_cost_per_token: 3e-7,
490
+ output_cost_per_token: 25e-7,
491
+ litellm_provider: "gemini",
492
+ mode: "chat"
493
+ },
278
494
  "gpt-3.5-turbo": {
279
495
  input_cost_per_token: 5e-7,
280
496
  output_cost_per_token: 15e-7,
@@ -449,18 +665,6 @@ var cost_map_default = {
449
665
  litellm_provider: "openai",
450
666
  mode: "chat"
451
667
  },
452
- "gpt-4o-mini-realtime-preview": {
453
- input_cost_per_token: 6e-7,
454
- output_cost_per_token: 24e-7,
455
- litellm_provider: "openai",
456
- mode: "chat"
457
- },
458
- "gpt-4o-mini-realtime-preview-2024-12-17": {
459
- input_cost_per_token: 6e-7,
460
- output_cost_per_token: 24e-7,
461
- litellm_provider: "openai",
462
- mode: "chat"
463
- },
464
668
  "gpt-4o-mini-search-preview": {
465
669
  input_cost_per_token: 15e-8,
466
670
  output_cost_per_token: 6e-7,
@@ -473,24 +677,6 @@ var cost_map_default = {
473
677
  litellm_provider: "openai",
474
678
  mode: "chat"
475
679
  },
476
- "gpt-4o-realtime-preview": {
477
- input_cost_per_token: 5e-6,
478
- output_cost_per_token: 2e-5,
479
- litellm_provider: "openai",
480
- mode: "chat"
481
- },
482
- "gpt-4o-realtime-preview-2024-12-17": {
483
- input_cost_per_token: 5e-6,
484
- output_cost_per_token: 2e-5,
485
- litellm_provider: "openai",
486
- mode: "chat"
487
- },
488
- "gpt-4o-realtime-preview-2025-06-03": {
489
- input_cost_per_token: 5e-6,
490
- output_cost_per_token: 2e-5,
491
- litellm_provider: "openai",
492
- mode: "chat"
493
- },
494
680
  "gpt-4o-search-preview": {
495
681
  input_cost_per_token: 25e-7,
496
682
  output_cost_per_token: 1e-5,
@@ -713,60 +899,6 @@ var cost_map_default = {
713
899
  litellm_provider: "openai",
714
900
  mode: "chat"
715
901
  },
716
- "gpt-realtime": {
717
- input_cost_per_token: 4e-6,
718
- output_cost_per_token: 16e-6,
719
- litellm_provider: "openai",
720
- mode: "chat"
721
- },
722
- "gpt-realtime-1.5": {
723
- input_cost_per_token: 4e-6,
724
- output_cost_per_token: 16e-6,
725
- litellm_provider: "openai",
726
- mode: "chat"
727
- },
728
- "gpt-realtime-2": {
729
- input_cost_per_token: 4e-6,
730
- output_cost_per_token: 16e-6,
731
- litellm_provider: "openai",
732
- mode: "chat"
733
- },
734
- "gpt-realtime-2.1": {
735
- input_cost_per_token: 4e-6,
736
- output_cost_per_token: 24e-6,
737
- litellm_provider: "openai",
738
- mode: "chat"
739
- },
740
- "gpt-realtime-2.1-mini": {
741
- input_cost_per_token: 6e-7,
742
- output_cost_per_token: 24e-7,
743
- litellm_provider: "openai",
744
- mode: "chat"
745
- },
746
- "gpt-realtime-2025-08-28": {
747
- input_cost_per_token: 4e-6,
748
- output_cost_per_token: 16e-6,
749
- litellm_provider: "openai",
750
- mode: "chat"
751
- },
752
- "gpt-realtime-mini": {
753
- input_cost_per_token: 6e-7,
754
- output_cost_per_token: 24e-7,
755
- litellm_provider: "openai",
756
- mode: "chat"
757
- },
758
- "gpt-realtime-mini-2025-10-06": {
759
- input_cost_per_token: 6e-7,
760
- output_cost_per_token: 24e-7,
761
- litellm_provider: "openai",
762
- mode: "chat"
763
- },
764
- "gpt-realtime-mini-2025-12-15": {
765
- input_cost_per_token: 6e-7,
766
- output_cost_per_token: 24e-7,
767
- litellm_provider: "openai",
768
- mode: "chat"
769
- },
770
902
  "llama-3.1-8b-instant": {
771
903
  input_cost_per_token: 5e-8,
772
904
  output_cost_per_token: 8e-8,
@@ -963,13 +1095,26 @@ var BudgetGuard = class {
963
1095
  failClosed;
964
1096
  nearLimitBps;
965
1097
  onBlock;
966
- /** Cost of the most recent priced call, used to predict the next one. */
967
- lastCost = 0;
1098
+ /**
1099
+ * Costs of the most recent priced LLM call and tool call, tracked
1100
+ * SEPARATELY: the default next-call prediction is the max of the two, so a
1101
+ * cheap tool call can't shrink the estimate right before an expensive LLM
1102
+ * call (or vice versa) — conservative beats one-call-too-late.
1103
+ */
1104
+ lastLlmCost = 0;
1105
+ lastToolCost = 0;
968
1106
  /** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */
969
1107
  reserved = 0;
970
1108
  /** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */
971
1109
  spendEvents = [];
972
1110
  maxLogEvents;
1111
+ /**
1112
+ * Per-tool running totals (settleTool/recordTool) — the tool side of the one
1113
+ * shared ceiling, exposed via the toolCosts getter. null-prototype: tool
1114
+ * names are caller-supplied strings, so a "__proto__" name is stored as
1115
+ * plain data instead of mutating the object's prototype.
1116
+ */
1117
+ toolCostTotals = /* @__PURE__ */ Object.create(null);
973
1118
  /**
974
1119
  * @param limitUsd the spend ceiling, in USD. `0` blocks the very first call.
975
1120
  */
@@ -1001,7 +1146,8 @@ var BudgetGuard = class {
1001
1146
  * Throw {@link BudgetExceeded} if the next call would cross the ceiling.
1002
1147
  *
1003
1148
  * Call this immediately before each LLM request. The "next call" is estimated
1004
- * from the last recorded call's cost (override with `estimatedNextCost`); the
1149
+ * conservatively as the costlier of the last LLM call and the last tool call
1150
+ * (override with `estimatedNextCost`); the
1005
1151
  * first call is always allowed unless the ceiling is already met. In-flight
1006
1152
  * reservations count toward the total, so this stays correct alongside
1007
1153
  * {@link BudgetGuard.reserve}.
@@ -1010,7 +1156,7 @@ var BudgetGuard = class {
1010
1156
  * `settle()`, which hold the estimate across the await.
1011
1157
  */
1012
1158
  check(estimatedNextCost) {
1013
- const rawEstimate = estimatedNextCost === void 0 ? this.lastCost : estimatedNextCost;
1159
+ const rawEstimate = estimatedNextCost === void 0 ? this.defaultEstimate() : estimatedNextCost;
1014
1160
  if (!Number.isFinite(rawEstimate)) {
1015
1161
  throw new RangeError(
1016
1162
  `estimatedNextCost must be a finite number, got ${rawEstimate}`
@@ -1031,10 +1177,11 @@ var BudgetGuard = class {
1031
1177
  * the same stale total. Throws {@link BudgetExceeded} (without reserving) if
1032
1178
  * the reservation would cross the ceiling. Returns the reservation handle to
1033
1179
  * pass to {@link BudgetGuard.settle} (or {@link BudgetGuard.release} on error).
1034
- * `estimatedCost` defaults to the last call's cost.
1180
+ * `estimatedCost` defaults to the costlier of the last LLM call and the last
1181
+ * tool call.
1035
1182
  */
1036
1183
  reserve(estimatedCost) {
1037
- const rawEstimate = estimatedCost === void 0 ? this.lastCost : estimatedCost;
1184
+ const rawEstimate = estimatedCost === void 0 ? this.defaultEstimate() : estimatedCost;
1038
1185
  if (!Number.isFinite(rawEstimate)) {
1039
1186
  throw new RangeError(
1040
1187
  `estimatedCost must be a finite number, got ${rawEstimate}`
@@ -1086,13 +1233,13 @@ var BudgetGuard = class {
1086
1233
  throw err;
1087
1234
  }
1088
1235
  if (reserved) {
1089
- this.reserved = Math.max(0, this.reserved - reserved);
1236
+ this.consumeReservation(reserved);
1090
1237
  }
1091
1238
  this.spentUsd += cost;
1092
1239
  if (this.spentUsd - this.limitUsd > 0 && this.spentUsd - this.limitUsd < EPS) {
1093
1240
  this.spentUsd = this.limitUsd;
1094
1241
  }
1095
- this.lastCost = cost;
1242
+ this.lastLlmCost = cost;
1096
1243
  this.appendEvent({
1097
1244
  timestamp: Date.now() / 1e3,
1098
1245
  kind: "llm",
@@ -1122,24 +1269,64 @@ var BudgetGuard = class {
1122
1269
  });
1123
1270
  }
1124
1271
  /**
1125
- * Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.
1272
+ * Atomically check the ceiling AND hold a tool call's cost in flight.
1126
1273
  *
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`.
1274
+ * The tool-spend counterpart of {@link BudgetGuard.reserve} — and STRONGER
1275
+ * than the LLM path, because a paid tool's price is usually known exactly
1276
+ * before the call, so the pre-call hard-stop is precise rather than
1277
+ * estimated:
1278
+ *
1279
+ * const handle = guard.reserveTool(0.02); // throws BEFORE Apollo runs
1280
+ * const result = await apollo.peopleLookup(...);
1281
+ * guard.settleTool("apollo.people_lookup", 0.02, { reserved: handle });
1282
+ *
1283
+ * Throws {@link BudgetExceeded} (without reserving) if the call would cross
1284
+ * the ceiling. The estimate is required — tools have no last-cost prediction
1285
+ * worth falling back to. Pass the returned handle to
1286
+ * {@link BudgetGuard.settleTool}, or {@link BudgetGuard.release} on failure.
1134
1287
  */
1135
- recordTool(tool, costUsd, options = {}) {
1288
+ reserveTool(estimatedCost) {
1289
+ if (estimatedCost === void 0) {
1290
+ throw new RangeError("reserveTool requires an estimated cost, got undefined");
1291
+ }
1292
+ if (!Number.isFinite(estimatedCost) || estimatedCost < 0) {
1293
+ throw new RangeError(
1294
+ `estimatedCost must be a finite, non-negative number, got ${estimatedCost}`
1295
+ );
1296
+ }
1297
+ return this.reserve(estimatedCost);
1298
+ }
1299
+ /**
1300
+ * Release a reservation and record a tool call's actual cost.
1301
+ *
1302
+ * `recordTool` is `settleTool` with no reservation. The caller supplies the
1303
+ * cost — tools have no token usage to price. Accrues into the same
1304
+ * `spentUsd` ceiling as tokens, tallies the per-tool total
1305
+ * ({@link BudgetGuard.toolCosts}), updates the tool side of the next-call
1306
+ * estimate (tracked separately from the LLM side; the default prediction is
1307
+ * the max of the two, so a tool-hammering loop's plain `check()` stops
1308
+ * BEFORE the crossing call without a cheap tool shrinking the LLM
1309
+ * prediction), and appends
1310
+ * a `kind: "tool"` {@link SpendEvent} to {@link BudgetGuard.spendLog}.
1311
+ * Returns `costUsd`.
1312
+ */
1313
+ settleTool(tool, costUsd, options = {}) {
1136
1314
  if (!Number.isFinite(costUsd) || costUsd < 0) {
1137
1315
  throw new RangeError(`costUsd must be a finite, non-negative number, got ${costUsd}`);
1138
1316
  }
1317
+ const reserved = options.reserved ?? 0;
1318
+ if (!Number.isFinite(reserved) || reserved < 0) {
1319
+ throw new RangeError(`reserved must be a finite, non-negative number, got ${reserved}`);
1320
+ }
1321
+ if (reserved) {
1322
+ this.consumeReservation(reserved);
1323
+ }
1139
1324
  this.spentUsd += costUsd;
1140
1325
  if (this.spentUsd - this.limitUsd > 0 && this.spentUsd - this.limitUsd < EPS) {
1141
1326
  this.spentUsd = this.limitUsd;
1142
1327
  }
1328
+ this.lastToolCost = costUsd;
1329
+ this.toolCostTotals[tool] = (this.toolCostTotals[tool] ?? 0) + costUsd;
1143
1330
  this.appendEvent({
1144
1331
  timestamp: Date.now() / 1e3,
1145
1332
  kind: "tool",
@@ -1147,10 +1334,22 @@ var BudgetGuard = class {
1147
1334
  promptTokens: null,
1148
1335
  completionTokens: null,
1149
1336
  costUsd,
1150
- ...options.label !== void 0 ? { label: options.label } : {}
1337
+ ...options.label !== void 0 ? { label: options.label } : {},
1338
+ ...reserved ? { reserved } : {}
1151
1339
  });
1152
1340
  return costUsd;
1153
1341
  }
1342
+ /**
1343
+ * Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.
1344
+ *
1345
+ * Post-hoc accrual for costs only known after the call (metered APIs); when
1346
+ * the price is known up front, {@link BudgetGuard.reserveTool} /
1347
+ * {@link BudgetGuard.settleTool} give the stronger pre-call hard-stop. See
1348
+ * `settleTool` for the full contract. Returns `costUsd`.
1349
+ */
1350
+ recordTool(tool, costUsd, options = {}) {
1351
+ return this.settleTool(tool, costUsd, { reserved: 0, label: options.label });
1352
+ }
1154
1353
  /**
1155
1354
  * Drop an in-flight reservation without recording spend (e.g. the call failed
1156
1355
  * before producing usage). Safe to call with `0`.
@@ -1160,16 +1359,25 @@ var BudgetGuard = class {
1160
1359
  throw new RangeError(`reserved must be a finite, non-negative number, got ${reserved}`);
1161
1360
  }
1162
1361
  if (!reserved) return;
1163
- this.reserved = Math.max(0, this.reserved - reserved);
1362
+ this.consumeReservation(reserved);
1164
1363
  }
1165
1364
  /** USD left before the ceiling, net of in-flight reservations (never negative). */
1166
1365
  get remainingUsd() {
1167
1366
  return Math.max(0, this.limitUsd - this.spentUsd - this.reserved);
1168
1367
  }
1368
+ /**
1369
+ * Per-tool running USD totals, keyed by the name given to `settleTool()` /
1370
+ * `recordTool()` — e.g. `{"apollo.people_lookup": 0.42, "exa.search": 0.11}`.
1371
+ * Makes the token/tool split of the one shared ceiling inspectable
1372
+ * (`spentUsd - sum of toolCosts` is the token side). Returns a snapshot copy.
1373
+ */
1374
+ get toolCosts() {
1375
+ return { ...this.toolCostTotals };
1376
+ }
1169
1377
  /**
1170
1378
  * 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.
1379
+ * `record()` / `settle()` / `recordTool()` / `settleTool()`. Returns a
1380
+ * snapshot copy: mutating it cannot corrupt the ledger.
1173
1381
  */
1174
1382
  get spendLog() {
1175
1383
  return [...this.spendEvents];
@@ -1200,6 +1408,34 @@ var BudgetGuard = class {
1200
1408
  `;
1201
1409
  }).join("");
1202
1410
  }
1411
+ /**
1412
+ * The default next-call prediction when the caller supplies no estimate.
1413
+ * Conservative: the costlier of the last LLM call and the last tool call — a
1414
+ * mixed loop predicts the pricier kind, which at worst blocks one call early
1415
+ * (fail-closed) rather than letting a crossing call through because the LAST
1416
+ * event happened to be cheap.
1417
+ */
1418
+ defaultEstimate() {
1419
+ return Math.max(this.lastLlmCost, this.lastToolCost);
1420
+ }
1421
+ /**
1422
+ * Subtract a settled/released hold from the in-flight tally. A handle larger
1423
+ * than EVERYTHING currently held cannot have come from a matching
1424
+ * `reserve()` — throwing beats silently clamping, which would free OTHER
1425
+ * callers' holds and fail the ceiling open. The epsilon absorbs float dust
1426
+ * from accumulating and draining many holds; per-caller over-release (a
1427
+ * handle within the total but larger than the caller's own hold) is
1428
+ * undetectable without per-handle tracking and remains the caller's
1429
+ * responsibility.
1430
+ */
1431
+ consumeReservation(reserved) {
1432
+ if (reserved > this.reserved + EPS) {
1433
+ throw new RangeError(
1434
+ `reserved handle (${reserved}) exceeds total in-flight reservations (${this.reserved}) \u2014 a handle must come from a matching reserve()`
1435
+ );
1436
+ }
1437
+ this.reserved = Math.max(0, this.reserved - reserved);
1438
+ }
1203
1439
  appendEvent(event) {
1204
1440
  this.spendEvents.push(Object.freeze(event));
1205
1441
  if (this.maxLogEvents !== void 0 && this.spendEvents.length > this.maxLogEvents) {