floe-guard 0.10.0 → 0.11.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 CHANGED
@@ -27,10 +27,16 @@ __export(index_exports, {
27
27
  LatencyBudget: () => LatencyBudget,
28
28
  TokenBudgetExceeded: () => TokenBudgetExceeded,
29
29
  UnpriceableModelError: () => UnpriceableModelError,
30
+ UnpriceableVoiceError: () => UnpriceableVoiceError,
30
31
  budgetGuardMiddleware: () => budgetGuardMiddleware,
32
+ gates: () => gates_exports,
33
+ lookupVoiceRate: () => lookupVoiceRate,
31
34
  priceTokens: () => priceTokens,
35
+ priceVoiceLeg: () => priceVoiceLeg,
32
36
  pricing: () => pricing_exports,
33
37
  resolvePrice: () => resolvePrice,
38
+ resolveVoiceRate: () => resolveVoiceRate,
39
+ voiceLegCost: () => voiceLegCost,
34
40
  withBudgetRetry: () => withBudgetRetry
35
41
  });
36
42
  module.exports = __toCommonJS(index_exports);
@@ -81,6 +87,19 @@ var UnpriceableModelError = class extends FloeGuardError {
81
87
  this.model = model;
82
88
  }
83
89
  };
90
+ var UnpriceableVoiceError = class extends FloeGuardError {
91
+ vendor;
92
+ mode;
93
+ constructor(vendor, mode) {
94
+ const shown = vendor === null ? "None" : `'${vendor}'`;
95
+ super(
96
+ `Cannot price ${mode} vendor ${shown}: not in the bundled voice cost map (or its entry has the wrong unit for a ${mode} leg) and no per-unit override was given. The guard cannot enforce a budget on spend it cannot measure. Pass a per-unit rate to enable enforcement.`
97
+ );
98
+ this.name = "UnpriceableVoiceError";
99
+ this.vendor = vendor;
100
+ this.mode = mode;
101
+ }
102
+ };
84
103
  function roundHalfUp(ms) {
85
104
  return Math.floor(ms + 0.5);
86
105
  }
@@ -1239,6 +1258,12 @@ function isReservation(h) {
1239
1258
  var BudgetGuard = class {
1240
1259
  limitUsd;
1241
1260
  spentUsd = 0;
1261
+ /**
1262
+ * Wall-clock start (ms since epoch) of this guard's spend window, for the $/min
1263
+ * burn rate (advisory().burnRateUsdPerMin). One guard per call/turn ⇒ a per-call
1264
+ * rate. Set at construction; tests set it directly to control elapsed time.
1265
+ */
1266
+ createdAtMs = Date.now();
1242
1267
  priceOverrides;
1243
1268
  failClosed;
1244
1269
  nearLimitBps;
@@ -1779,6 +1804,8 @@ var BudgetGuard = class {
1779
1804
  const remainingUsd = Math.max(0, this.limitUsd - this.spentUsd);
1780
1805
  const expectedCost = Math.max(this.lastLlmCost, this.lastToolCost);
1781
1806
  const estCallsRemaining = expectedCost > 0 ? Math.floor(remainingUsd / expectedCost + 1e-9) : null;
1807
+ const elapsedMin = (Date.now() - this.createdAtMs) / 6e4;
1808
+ const burnRateUsdPerMin = elapsedMin > 0 ? this.spentUsd / elapsedMin : null;
1782
1809
  let tokenUsedBps = null;
1783
1810
  let remainingTokens = null;
1784
1811
  let nearToken = false;
@@ -1832,7 +1859,8 @@ var BudgetGuard = class {
1832
1859
  tokenUsedBps,
1833
1860
  remainingTokens,
1834
1861
  stepRemainingUsd,
1835
- stepRemainingTokens
1862
+ stepRemainingTokens,
1863
+ burnRateUsdPerMin
1836
1864
  };
1837
1865
  }
1838
1866
  };
@@ -2031,6 +2059,107 @@ async function nextPlan(guard, error, current, onDegrade) {
2031
2059
  guard.check(current.estimatedCost);
2032
2060
  return current;
2033
2061
  }
2062
+
2063
+ // src/voice-pricing.ts
2064
+ var VOICE_MAP = cost_map_default["__voice__"] ?? {};
2065
+ var unitForMode = {
2066
+ stt: "usd_per_second",
2067
+ tts: "usd_per_1k_chars",
2068
+ telephony: "usd_per_minute"
2069
+ };
2070
+ function finiteNonNegative(value) {
2071
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
2072
+ }
2073
+ function lookupVoiceRate(model, mode) {
2074
+ if (model === null || model === void 0) return null;
2075
+ const entry = Object.prototype.hasOwnProperty.call(VOICE_MAP, model) ? VOICE_MAP[model] : void 0;
2076
+ if (!entry) return null;
2077
+ if (entry.mode !== mode) return null;
2078
+ if (entry.unit !== unitForMode[mode]) return null;
2079
+ const rate = entry.rate;
2080
+ if (!finiteNonNegative(rate)) return null;
2081
+ return rate;
2082
+ }
2083
+ function resolveVoiceRate(model, mode, override) {
2084
+ if (override !== void 0 && override !== null) {
2085
+ if (!finiteNonNegative(override)) {
2086
+ throw new RangeError(
2087
+ `voice ${mode} override must be a finite, non-negative number, got ${override}`
2088
+ );
2089
+ }
2090
+ return { mode, unit: unitForMode[mode], rate: override, source: "override" };
2091
+ }
2092
+ const rate = lookupVoiceRate(model, mode);
2093
+ if (rate === null) throw new UnpriceableVoiceError(model ?? null, mode);
2094
+ return { mode, unit: unitForMode[mode], rate, source: "cost_map" };
2095
+ }
2096
+ function voiceLegCost(mode, quantity, rate) {
2097
+ if (!Number.isFinite(quantity)) {
2098
+ throw new RangeError(`voice ${mode} quantity must be a finite number, got ${quantity}`);
2099
+ }
2100
+ const q = Math.max(0, quantity);
2101
+ if (mode === "stt") return q * rate;
2102
+ if (mode === "tts") return q / 1e3 * rate;
2103
+ if (mode === "telephony") return q * rate;
2104
+ throw new RangeError(`unknown voice mode ${mode}`);
2105
+ }
2106
+ function priceVoiceLeg(mode, quantity, options = {}) {
2107
+ const model = options.model ?? null;
2108
+ const override = options.override ?? null;
2109
+ if (model === null && override === null) return null;
2110
+ const resolved = resolveVoiceRate(model, mode, override);
2111
+ return voiceLegCost(mode, quantity, resolved.rate);
2112
+ }
2113
+
2114
+ // src/gates.ts
2115
+ var gates_exports = {};
2116
+ __export(gates_exports, {
2117
+ budgetExhausted: () => budgetExhausted,
2118
+ preCall: () => preCall,
2119
+ retell: () => retell,
2120
+ vapi: () => vapi
2121
+ });
2122
+ function budgetExhausted(guard, options = {}) {
2123
+ const estimatedCallUsd = options.estimatedCallUsd === void 0 ? 0 : options.estimatedCallUsd;
2124
+ if (!Number.isFinite(estimatedCallUsd) || estimatedCallUsd < 0) {
2125
+ throw new RangeError(
2126
+ `estimatedCallUsd must be a finite, non-negative number, got ${estimatedCallUsd}`
2127
+ );
2128
+ }
2129
+ const remainingUsd = guard.remainingUsd;
2130
+ return remainingUsd <= 0 || remainingUsd < estimatedCallUsd;
2131
+ }
2132
+ function preCall(guard, options = {}) {
2133
+ return !budgetExhausted(guard, options);
2134
+ }
2135
+ function retell(guard, options = {}) {
2136
+ if (budgetExhausted(guard, { estimatedCallUsd: options.estimatedCallUsd })) {
2137
+ return { call_inbound: { reject: true } };
2138
+ }
2139
+ const safeAdmit = { ...options.admit ?? {} };
2140
+ delete safeAdmit.reject;
2141
+ return { call_inbound: safeAdmit };
2142
+ }
2143
+ function vapi(guard, options = {}) {
2144
+ const {
2145
+ assistant,
2146
+ assistantId,
2147
+ errorMessage = "Sorry, this agent is out of budget right now.",
2148
+ estimatedCallUsd
2149
+ } = options;
2150
+ if (budgetExhausted(guard, { estimatedCallUsd })) {
2151
+ return { error: errorMessage };
2152
+ }
2153
+ if (assistantId !== void 0 && assistantId !== null) {
2154
+ return { assistantId };
2155
+ }
2156
+ if (assistant !== void 0 && assistant !== null) {
2157
+ return { assistant };
2158
+ }
2159
+ throw new RangeError(
2160
+ "vapi() admitted the call but has nothing to return: pass assistant or assistantId for the admit path."
2161
+ );
2162
+ }
2034
2163
  // Annotate the CommonJS export names for ESM import in node:
2035
2164
  0 && (module.exports = {
2036
2165
  BudgetExceeded,
@@ -2040,10 +2169,16 @@ async function nextPlan(guard, error, current, onDegrade) {
2040
2169
  LatencyBudget,
2041
2170
  TokenBudgetExceeded,
2042
2171
  UnpriceableModelError,
2172
+ UnpriceableVoiceError,
2043
2173
  budgetGuardMiddleware,
2174
+ gates,
2175
+ lookupVoiceRate,
2044
2176
  priceTokens,
2177
+ priceVoiceLeg,
2045
2178
  pricing,
2046
2179
  resolvePrice,
2180
+ resolveVoiceRate,
2181
+ voiceLegCost,
2047
2182
  withBudgetRetry
2048
2183
  });
2049
2184
  //# sourceMappingURL=index.cjs.map