floe-guard 0.13.0 → 0.13.2

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.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { B as BudgetGuard, b as BudgetAdvisory } from './guard-BXtXoark.cjs';
2
- export { a as BudgetExceeded, c as BudgetGuardOptions, d as BudgetReservation, D as DeadlineExceeded, F as FloeGuardError, L as LedgerSyncError, M as ManualPrice, P as PricedModel, R as ReservationHandle, S as SpendEvent, T as TokenBudgetExceeded, U as UnpriceableModelError, e as UnpriceableVoiceError, p as priceTokens, f as pricing, r as resolvePrice } from './guard-BXtXoark.cjs';
1
+ import { B as BudgetGuard, b as BudgetAdvisory } from './guard-Ct38zQZS.cjs';
2
+ export { a as BudgetExceeded, c as BudgetGuardOptions, d as BudgetReservation, D as DeadlineExceeded, F as FloeGuardError, L as LedgerSyncError, M as ManualPrice, P as PricedModel, R as ReservationHandle, S as SpendEvent, T as TokenBudgetExceeded, U as UnpriceableModelError, e as UnpriceableVoiceError, p as priceTokens, f as pricing, r as resolvePrice } from './guard-Ct38zQZS.cjs';
3
3
 
4
4
  /**
5
5
  * LatencyBudget — a cumulative tool-chain deadline, sibling to BudgetGuard.
@@ -142,7 +142,9 @@ declare function pushLedger(jsonl: string, apiKey?: string, opts?: {
142
142
  * Reserving before the await is what makes parallel calls (`Promise.all` over
143
143
  * several generations) honour the ceiling: each holds its slice instead of all
144
144
  * reading the same stale total (issue #18). The reservation is released if the
145
- * call throws, or if a stream ends without reporting usage.
145
+ * call throws, is cancelled by the consumer, or if a stream completes without a
146
+ * usage-bearing finish part — in the last case the stream is also rejected with
147
+ * an error rather than treated as a free ($0) call (fail-closed).
146
148
  *
147
149
  * The model id used for pricing comes from `model.modelId`.
148
150
  */
@@ -205,7 +207,14 @@ interface BudgetRetryOptions<T> {
205
207
  maxAttempts?: number;
206
208
  /** Choose a cheaper retry plan when `guard.advisory().nearLimit` is true. */
207
209
  onDegrade?: (error: unknown, advisory: BudgetAdvisory) => RetryPlan<T> | Promise<RetryPlan<T> | undefined> | undefined;
208
- /** Decide whether an error is retryable. Defaults to all non-budget errors. */
210
+ /**
211
+ * Decide whether an error is retryable.
212
+ *
213
+ * Defaults to all non-{@link FloeGuardError} errors — every
214
+ * {@link FloeGuardError} (including {@link BudgetExceeded} and
215
+ * {@link UnpriceableModelError}) is treated as terminal because retrying a
216
+ * deterministic guard error cannot fix it and would only multiply LLM spend.
217
+ */
209
218
  retryIf?: (error: unknown) => boolean;
210
219
  }
211
220
  /**
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { B as BudgetGuard, b as BudgetAdvisory } from './guard-BXtXoark.js';
2
- export { a as BudgetExceeded, c as BudgetGuardOptions, d as BudgetReservation, D as DeadlineExceeded, F as FloeGuardError, L as LedgerSyncError, M as ManualPrice, P as PricedModel, R as ReservationHandle, S as SpendEvent, T as TokenBudgetExceeded, U as UnpriceableModelError, e as UnpriceableVoiceError, p as priceTokens, f as pricing, r as resolvePrice } from './guard-BXtXoark.js';
1
+ import { B as BudgetGuard, b as BudgetAdvisory } from './guard-Ct38zQZS.js';
2
+ export { a as BudgetExceeded, c as BudgetGuardOptions, d as BudgetReservation, D as DeadlineExceeded, F as FloeGuardError, L as LedgerSyncError, M as ManualPrice, P as PricedModel, R as ReservationHandle, S as SpendEvent, T as TokenBudgetExceeded, U as UnpriceableModelError, e as UnpriceableVoiceError, p as priceTokens, f as pricing, r as resolvePrice } from './guard-Ct38zQZS.js';
3
3
 
4
4
  /**
5
5
  * LatencyBudget — a cumulative tool-chain deadline, sibling to BudgetGuard.
@@ -142,7 +142,9 @@ declare function pushLedger(jsonl: string, apiKey?: string, opts?: {
142
142
  * Reserving before the await is what makes parallel calls (`Promise.all` over
143
143
  * several generations) honour the ceiling: each holds its slice instead of all
144
144
  * reading the same stale total (issue #18). The reservation is released if the
145
- * call throws, or if a stream ends without reporting usage.
145
+ * call throws, is cancelled by the consumer, or if a stream completes without a
146
+ * usage-bearing finish part — in the last case the stream is also rejected with
147
+ * an error rather than treated as a free ($0) call (fail-closed).
146
148
  *
147
149
  * The model id used for pricing comes from `model.modelId`.
148
150
  */
@@ -205,7 +207,14 @@ interface BudgetRetryOptions<T> {
205
207
  maxAttempts?: number;
206
208
  /** Choose a cheaper retry plan when `guard.advisory().nearLimit` is true. */
207
209
  onDegrade?: (error: unknown, advisory: BudgetAdvisory) => RetryPlan<T> | Promise<RetryPlan<T> | undefined> | undefined;
208
- /** Decide whether an error is retryable. Defaults to all non-budget errors. */
210
+ /**
211
+ * Decide whether an error is retryable.
212
+ *
213
+ * Defaults to all non-{@link FloeGuardError} errors — every
214
+ * {@link FloeGuardError} (including {@link BudgetExceeded} and
215
+ * {@link UnpriceableModelError}) is treated as terminal because retrying a
216
+ * deterministic guard error cannot fix it and would only multiply LLM spend.
217
+ */
209
218
  retryIf?: (error: unknown) => boolean;
210
219
  }
211
220
  /**
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  priceVoiceLeg,
14
14
  resolveVoiceRate,
15
15
  voiceLegCost
16
- } from "./chunk-5Q5VQC6J.js";
16
+ } from "./chunk-TAK6I7CN.js";
17
17
 
18
18
  // src/sync.ts
19
19
  var FLOE_API_KEY_ENV = "FLOE_API_KEY";
@@ -772,19 +772,21 @@ var BudgetGuard = class {
772
772
  }
773
773
  /**
774
774
  * The ONE choke point, across two dimensions and two scopes. Returns the first
775
- * ceiling that blocks as `[dimension, scope]` — dimension `"usd" | "tokens"`,
776
- * scope `"aggregate" | "step"` — or `null` if the call fits everywhere.
777
- * Aggregate is checked before step (the guard-wide ceiling is the hard limit).
775
+ * ceiling that blocks as `[dimension, scope, spent, limit]` — dimension
776
+ * `"usd" | "tokens"`, scope `"aggregate" | "step"` — or `null` if the call
777
+ * fits everywhere. Aggregate is checked before step (the guard-wide ceiling
778
+ * is the hard limit). `spent` / `limit` are captured here so `raiseBlock`
779
+ * (called outside the lock) reads no shared state and stays race-free.
778
780
  */
779
781
  blockingCross(estimateUsd, estimateTokens) {
780
782
  const committed = this.spentUsd + this.reserved;
781
783
  if (committed > this.limitUsd - EPS || committed + estimateUsd > this.limitUsd + EPS) {
782
- return ["usd", "aggregate"];
784
+ return ["usd", "aggregate", this.spentUsd, this.limitUsd];
783
785
  }
784
786
  if (this.tokenLimit !== null) {
785
787
  const committedT = this.spentTokens + this.reservedTokens;
786
788
  if (committedT >= this.tokenLimit || committedT + estimateTokens > this.tokenLimit) {
787
- return ["tokens", "aggregate"];
789
+ return ["tokens", "aggregate", committedT, this.tokenLimit];
788
790
  }
789
791
  }
790
792
  const step = this.steps.length ? this.steps[this.steps.length - 1] : null;
@@ -792,36 +794,26 @@ var BudgetGuard = class {
792
794
  if (step.maxUsd !== null) {
793
795
  const sCommitted = step.spentUsd + step.reservedUsd;
794
796
  if (sCommitted > step.maxUsd - EPS || sCommitted + estimateUsd > step.maxUsd + EPS) {
795
- return ["usd", "step"];
797
+ return ["usd", "step", sCommitted, step.maxUsd];
796
798
  }
797
799
  }
798
800
  if (step.maxTokens !== null) {
799
801
  const sCommittedT = step.spentTokens + step.reservedTokens;
800
802
  if (sCommittedT >= step.maxTokens || sCommittedT + estimateTokens > step.maxTokens) {
801
- return ["tokens", "step"];
803
+ return ["tokens", "step", sCommittedT, step.maxTokens];
802
804
  }
803
805
  }
804
806
  }
805
807
  return null;
806
808
  }
807
- /** Notify + throw the right error for a [dimension, scope] block. */
809
+ /** Notify + throw the right error for a [dimension, scope, spent, limit] block. */
808
810
  raiseBlock(blocked) {
809
- const [dimension, scope] = blocked;
811
+ const [dimension, scope, spent, limit] = blocked;
810
812
  if (dimension === "usd") {
811
- this.onBlock(this.spentUsd, this.limitUsd);
812
- throw new BudgetExceeded(this.spentUsd, this.limitUsd);
813
+ this.onBlock(spent, limit);
814
+ throw new BudgetExceeded(spent, limit);
813
815
  }
814
- let spentT;
815
- let limitT;
816
- if (scope === "step") {
817
- const step = this.steps[this.steps.length - 1];
818
- spentT = step.spentTokens + step.reservedTokens;
819
- limitT = step.maxTokens ?? 0;
820
- } else {
821
- spentT = this.spentTokens + this.reservedTokens;
822
- limitT = this.tokenLimit ?? 0;
823
- }
824
- throw new TokenBudgetExceeded(spentT, limitT, scope);
816
+ throw new TokenBudgetExceeded(Math.trunc(spent), Math.trunc(limit), scope);
825
817
  }
826
818
  /**
827
819
  * Scope a per-step USD and/or token cap for a **sequential agent loop**.
@@ -1090,6 +1082,9 @@ function budgetGuardMiddleware(guard) {
1090
1082
  if (!handled) {
1091
1083
  handled = true;
1092
1084
  guard.release(reserved);
1085
+ throw new Error(
1086
+ `Model '${model.modelId}' stream completed without a token-usage finish part \u2014 the budget guard cannot meter spend it cannot see, so this call is rejected rather than treated as free.`
1087
+ );
1093
1088
  }
1094
1089
  },
1095
1090
  cancel() {
@@ -1109,7 +1104,7 @@ function budgetGuardMiddleware(guard) {
1109
1104
 
1110
1105
  // src/retry.ts
1111
1106
  function defaultRetryIf(error) {
1112
- return !(error instanceof BudgetExceeded);
1107
+ return !(error instanceof FloeGuardError);
1113
1108
  }
1114
1109
  async function withBudgetRetry(guard, call, options = {}) {
1115
1110
  const maxAttempts = options.maxAttempts ?? 2;