floe-guard 0.9.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
  }
@@ -220,6 +239,12 @@ var cost_map_default = {
220
239
  litellm_provider: "anthropic",
221
240
  mode: "chat"
222
241
  },
242
+ "claude-opus-5": {
243
+ input_cost_per_token: 5e-6,
244
+ output_cost_per_token: 25e-6,
245
+ litellm_provider: "anthropic",
246
+ mode: "chat"
247
+ },
223
248
  "claude-sonnet-4-20250514": {
224
249
  input_cost_per_token: 3e-6,
225
250
  output_cost_per_token: 15e-6,
@@ -538,6 +563,18 @@ var cost_map_default = {
538
563
  litellm_provider: "gemini",
539
564
  mode: "chat"
540
565
  },
566
+ "gemini-robotics-er-1.6-preview": {
567
+ input_cost_per_token: 1e-6,
568
+ output_cost_per_token: 5e-6,
569
+ litellm_provider: "gemini",
570
+ mode: "chat"
571
+ },
572
+ "gemini-robotics-er-2-preview": {
573
+ input_cost_per_token: 2e-6,
574
+ output_cost_per_token: 1e-5,
575
+ litellm_provider: "gemini",
576
+ mode: "chat"
577
+ },
541
578
  "gpt-3.5-turbo": {
542
579
  input_cost_per_token: 5e-7,
543
580
  output_cost_per_token: 15e-7,
@@ -893,8 +930,8 @@ var cost_map_default = {
893
930
  mode: "chat"
894
931
  },
895
932
  "gpt-5.6-luna": {
896
- input_cost_per_token: 1e-6,
897
- output_cost_per_token: 6e-6,
933
+ input_cost_per_token: 2e-7,
934
+ output_cost_per_token: 12e-7,
898
935
  litellm_provider: "openai",
899
936
  mode: "chat"
900
937
  },
@@ -905,8 +942,8 @@ var cost_map_default = {
905
942
  mode: "chat"
906
943
  },
907
944
  "gpt-5.6-terra": {
908
- input_cost_per_token: 25e-7,
909
- output_cost_per_token: 15e-6,
945
+ input_cost_per_token: 2e-6,
946
+ output_cost_per_token: 12e-6,
910
947
  litellm_provider: "openai",
911
948
  mode: "chat"
912
949
  },
@@ -1059,6 +1096,86 @@ var cost_map_default = {
1059
1096
  output_cost_per_token: 0,
1060
1097
  litellm_provider: "openai",
1061
1098
  mode: "embedding"
1099
+ },
1100
+ __voice__: {
1101
+ "deepgram-nova-3": {
1102
+ mode: "stt",
1103
+ unit: "usd_per_second",
1104
+ rate: 1283333e-10,
1105
+ provider: "deepgram"
1106
+ },
1107
+ "deepgram-nova-3-multilingual": {
1108
+ mode: "stt",
1109
+ unit: "usd_per_second",
1110
+ rate: 1533333e-10,
1111
+ provider: "deepgram"
1112
+ },
1113
+ "deepgram-nova-3-base": {
1114
+ mode: "stt",
1115
+ unit: "usd_per_second",
1116
+ rate: 2416667e-10,
1117
+ provider: "deepgram"
1118
+ },
1119
+ "assemblyai-universal-streaming": {
1120
+ mode: "stt",
1121
+ unit: "usd_per_second",
1122
+ rate: 416667e-10,
1123
+ provider: "assemblyai"
1124
+ },
1125
+ "elevenlabs-multilingual-v2": {
1126
+ mode: "tts",
1127
+ unit: "usd_per_1k_chars",
1128
+ rate: 0.1,
1129
+ provider: "elevenlabs"
1130
+ },
1131
+ "elevenlabs-flash-v2.5": {
1132
+ mode: "tts",
1133
+ unit: "usd_per_1k_chars",
1134
+ rate: 0.05,
1135
+ provider: "elevenlabs"
1136
+ },
1137
+ "elevenlabs-turbo-v2.5": {
1138
+ mode: "tts",
1139
+ unit: "usd_per_1k_chars",
1140
+ rate: 0.05,
1141
+ provider: "elevenlabs"
1142
+ },
1143
+ "cartesia-sonic": {
1144
+ mode: "tts",
1145
+ unit: "usd_per_1k_chars",
1146
+ rate: 0.03,
1147
+ provider: "cartesia"
1148
+ },
1149
+ "cartesia-line": {
1150
+ mode: "telephony",
1151
+ unit: "usd_per_minute",
1152
+ rate: 0.06,
1153
+ provider: "cartesia"
1154
+ },
1155
+ "rime-mist-v2": {
1156
+ mode: "tts",
1157
+ unit: "usd_per_1k_chars",
1158
+ rate: 0.03,
1159
+ provider: "rime"
1160
+ },
1161
+ "twilio-us-inbound-local": {
1162
+ mode: "telephony",
1163
+ unit: "usd_per_minute",
1164
+ rate: 85e-4,
1165
+ provider: "twilio"
1166
+ },
1167
+ "twilio-us-outbound-local": {
1168
+ mode: "telephony",
1169
+ unit: "usd_per_minute",
1170
+ rate: 0.014,
1171
+ provider: "twilio"
1172
+ },
1173
+ "twilio-us-sip-inbound": {
1174
+ mode: "telephony",
1175
+ unit: "usd_per_minute",
1176
+ rate: 4e-3,
1177
+ provider: "twilio"
1178
+ }
1062
1179
  }
1063
1180
  };
1064
1181
 
@@ -1141,6 +1258,12 @@ function isReservation(h) {
1141
1258
  var BudgetGuard = class {
1142
1259
  limitUsd;
1143
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();
1144
1267
  priceOverrides;
1145
1268
  failClosed;
1146
1269
  nearLimitBps;
@@ -1681,6 +1804,8 @@ var BudgetGuard = class {
1681
1804
  const remainingUsd = Math.max(0, this.limitUsd - this.spentUsd);
1682
1805
  const expectedCost = Math.max(this.lastLlmCost, this.lastToolCost);
1683
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;
1684
1809
  let tokenUsedBps = null;
1685
1810
  let remainingTokens = null;
1686
1811
  let nearToken = false;
@@ -1734,7 +1859,8 @@ var BudgetGuard = class {
1734
1859
  tokenUsedBps,
1735
1860
  remainingTokens,
1736
1861
  stepRemainingUsd,
1737
- stepRemainingTokens
1862
+ stepRemainingTokens,
1863
+ burnRateUsdPerMin
1738
1864
  };
1739
1865
  }
1740
1866
  };
@@ -1933,6 +2059,107 @@ async function nextPlan(guard, error, current, onDegrade) {
1933
2059
  guard.check(current.estimatedCost);
1934
2060
  return current;
1935
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
+ }
1936
2163
  // Annotate the CommonJS export names for ESM import in node:
1937
2164
  0 && (module.exports = {
1938
2165
  BudgetExceeded,
@@ -1942,10 +2169,16 @@ async function nextPlan(guard, error, current, onDegrade) {
1942
2169
  LatencyBudget,
1943
2170
  TokenBudgetExceeded,
1944
2171
  UnpriceableModelError,
2172
+ UnpriceableVoiceError,
1945
2173
  budgetGuardMiddleware,
2174
+ gates,
2175
+ lookupVoiceRate,
1946
2176
  priceTokens,
2177
+ priceVoiceLeg,
1947
2178
  pricing,
1948
2179
  resolvePrice,
2180
+ resolveVoiceRate,
2181
+ voiceLegCost,
1949
2182
  withBudgetRetry
1950
2183
  });
1951
2184
  //# sourceMappingURL=index.cjs.map