floe-guard 0.17.0 → 0.18.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.
@@ -50,7 +50,7 @@ var UnpriceableLegError = class extends FloeGuardError {
50
50
  constructor(vendor, mode) {
51
51
  const shown = vendor === null ? "None" : `'${vendor}'`;
52
52
  super(
53
- `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.`
53
+ `Cannot price ${mode} vendor ${shown}: no per-call override, no usable entry in your rate card, and none in the bundled per-unit leg map \u2014 or an entry exists whose mode/unit is wrong for a ${mode} leg, which is refused rather than reinterpreted. The guard cannot enforce a budget on spend it cannot measure. Declare the rate you pay in your rate card (FLOE_RATE_CARD) or pass a per-unit override to enable enforcement.`
54
54
  );
55
55
  this.name = "UnpriceableVoiceError";
56
56
  this.vendor = vendor;
@@ -77,7 +77,7 @@ function preCall(guard, options = {}) {
77
77
  var cost_map_default = {
78
78
  __meta__: {
79
79
  generated_at: "2026-09-14",
80
- source: "LiteLLM public model prices (bundled snapshot); voice rates verified from vendor list pages"
80
+ source: "LiteLLM public model prices (bundled snapshot); per-unit leg rates (stt/tts/telephony/sms/ocr/gpu/avatar) hand-curated from vendor list pages, leg rates last retrieved 2026-09-18"
81
81
  },
82
82
  "chat-latest": {
83
83
  input_cost_per_token: 5e-6,
@@ -1287,7 +1287,7 @@ var cost_map_default = {
1287
1287
  litellm_provider: "openai",
1288
1288
  mode: "embedding"
1289
1289
  },
1290
- __voice__: {
1290
+ __legs__: {
1291
1291
  "deepgram-nova-3": {
1292
1292
  mode: "stt",
1293
1293
  unit: "usd_per_second",
@@ -1365,29 +1365,355 @@ var cost_map_default = {
1365
1365
  unit: "usd_per_minute",
1366
1366
  rate: 4e-3,
1367
1367
  provider: "twilio"
1368
+ },
1369
+ "twilio-sms-us-outbound": {
1370
+ mode: "sms",
1371
+ unit: "usd_per_segment",
1372
+ rate: 83e-4,
1373
+ provider: "twilio",
1374
+ source_url: "https://www.twilio.com/en-us/sms/pricing/us",
1375
+ retrieved_at: "2026-09-18"
1376
+ },
1377
+ "twilio-sms-us-inbound": {
1378
+ mode: "sms",
1379
+ unit: "usd_per_segment",
1380
+ rate: 83e-4,
1381
+ provider: "twilio",
1382
+ source_url: "https://www.twilio.com/en-us/sms/pricing/us",
1383
+ retrieved_at: "2026-09-18"
1384
+ },
1385
+ "telnyx-sms-us-outbound": {
1386
+ mode: "sms",
1387
+ unit: "usd_per_segment",
1388
+ rate: 4e-3,
1389
+ provider: "telnyx",
1390
+ source_url: "https://telnyx.com/pricing/messaging",
1391
+ retrieved_at: "2026-09-18"
1392
+ },
1393
+ "telnyx-sms-us-inbound": {
1394
+ mode: "sms",
1395
+ unit: "usd_per_segment",
1396
+ rate: 4e-3,
1397
+ provider: "telnyx",
1398
+ source_url: "https://telnyx.com/pricing/messaging",
1399
+ retrieved_at: "2026-09-18"
1400
+ },
1401
+ "modal-b300": {
1402
+ mode: "gpu",
1403
+ unit: "usd_per_gpu_second",
1404
+ rate: 1972e-6,
1405
+ provider: "modal",
1406
+ source_url: "https://modal.com/pricing",
1407
+ retrieved_at: "2026-09-18"
1408
+ },
1409
+ "modal-b200": {
1410
+ mode: "gpu",
1411
+ unit: "usd_per_gpu_second",
1412
+ rate: 1736e-6,
1413
+ provider: "modal",
1414
+ source_url: "https://modal.com/pricing",
1415
+ retrieved_at: "2026-09-18"
1416
+ },
1417
+ "modal-h200-sxm": {
1418
+ mode: "gpu",
1419
+ unit: "usd_per_gpu_second",
1420
+ rate: 1261e-6,
1421
+ provider: "modal",
1422
+ source_url: "https://modal.com/pricing",
1423
+ retrieved_at: "2026-09-18"
1424
+ },
1425
+ "modal-h100-sxm5": {
1426
+ mode: "gpu",
1427
+ unit: "usd_per_gpu_second",
1428
+ rate: 1097e-6,
1429
+ provider: "modal",
1430
+ source_url: "https://modal.com/pricing",
1431
+ retrieved_at: "2026-09-18"
1432
+ },
1433
+ "modal-rtx-pro-6000": {
1434
+ mode: "gpu",
1435
+ unit: "usd_per_gpu_second",
1436
+ rate: 842e-6,
1437
+ provider: "modal",
1438
+ source_url: "https://modal.com/pricing",
1439
+ retrieved_at: "2026-09-18"
1440
+ },
1441
+ "modal-a100-80gb": {
1442
+ mode: "gpu",
1443
+ unit: "usd_per_gpu_second",
1444
+ rate: 694e-6,
1445
+ provider: "modal",
1446
+ source_url: "https://modal.com/pricing",
1447
+ retrieved_at: "2026-09-18"
1448
+ },
1449
+ "modal-a100-40gb": {
1450
+ mode: "gpu",
1451
+ unit: "usd_per_gpu_second",
1452
+ rate: 583e-6,
1453
+ provider: "modal",
1454
+ source_url: "https://modal.com/pricing",
1455
+ retrieved_at: "2026-09-18"
1456
+ },
1457
+ "modal-l40s": {
1458
+ mode: "gpu",
1459
+ unit: "usd_per_gpu_second",
1460
+ rate: 542e-6,
1461
+ provider: "modal",
1462
+ source_url: "https://modal.com/pricing",
1463
+ retrieved_at: "2026-09-18"
1464
+ },
1465
+ "modal-a10": {
1466
+ mode: "gpu",
1467
+ unit: "usd_per_gpu_second",
1468
+ rate: 306e-6,
1469
+ provider: "modal",
1470
+ source_url: "https://modal.com/pricing",
1471
+ retrieved_at: "2026-09-18"
1472
+ },
1473
+ "modal-l4": {
1474
+ mode: "gpu",
1475
+ unit: "usd_per_gpu_second",
1476
+ rate: 222e-6,
1477
+ provider: "modal",
1478
+ source_url: "https://modal.com/pricing",
1479
+ retrieved_at: "2026-09-18"
1480
+ },
1481
+ "modal-t4": {
1482
+ mode: "gpu",
1483
+ unit: "usd_per_gpu_second",
1484
+ rate: 164e-6,
1485
+ provider: "modal",
1486
+ source_url: "https://modal.com/pricing",
1487
+ retrieved_at: "2026-09-18"
1488
+ },
1489
+ "gcp-vision-text-detection": {
1490
+ mode: "ocr",
1491
+ unit: "usd_per_page",
1492
+ rate: 15e-4,
1493
+ provider: "gcp",
1494
+ source_url: "https://cloud.google.com/vision/pricing",
1495
+ retrieved_at: "2026-09-18"
1496
+ },
1497
+ "gcp-vision-document-text-detection": {
1498
+ mode: "ocr",
1499
+ unit: "usd_per_page",
1500
+ rate: 15e-4,
1501
+ provider: "gcp",
1502
+ source_url: "https://cloud.google.com/vision/pricing",
1503
+ retrieved_at: "2026-09-18"
1504
+ },
1505
+ "aws-textract-detect-document-text": {
1506
+ mode: "ocr",
1507
+ unit: "usd_per_page",
1508
+ rate: 15e-4,
1509
+ provider: "aws",
1510
+ source_url: "https://aws.amazon.com/textract/pricing/",
1511
+ retrieved_at: "2026-09-18"
1512
+ },
1513
+ "aws-textract-analyze-forms": {
1514
+ mode: "ocr",
1515
+ unit: "usd_per_page",
1516
+ rate: 0.05,
1517
+ provider: "aws",
1518
+ source_url: "https://aws.amazon.com/textract/pricing/",
1519
+ retrieved_at: "2026-09-18"
1520
+ },
1521
+ "aws-textract-analyze-tables": {
1522
+ mode: "ocr",
1523
+ unit: "usd_per_page",
1524
+ rate: 0.015,
1525
+ provider: "aws",
1526
+ source_url: "https://aws.amazon.com/textract/pricing/",
1527
+ retrieved_at: "2026-09-18"
1528
+ },
1529
+ "tavus-cvi-starter": {
1530
+ mode: "avatar",
1531
+ unit: "usd_per_minute",
1532
+ rate: 0.37,
1533
+ provider: "tavus",
1534
+ source_url: "https://www.tavus.io/pricing",
1535
+ retrieved_at: "2026-09-18"
1536
+ },
1537
+ "gcp-vision-text-detection-high-volume": {
1538
+ mode: "ocr",
1539
+ unit: "usd_per_page",
1540
+ rate: 6e-4,
1541
+ provider: "gcp",
1542
+ source_url: "https://cloud.google.com/vision/pricing",
1543
+ retrieved_at: "2026-09-18"
1544
+ },
1545
+ "gcp-vision-document-text-detection-high-volume": {
1546
+ mode: "ocr",
1547
+ unit: "usd_per_page",
1548
+ rate: 6e-4,
1549
+ provider: "gcp",
1550
+ source_url: "https://cloud.google.com/vision/pricing",
1551
+ retrieved_at: "2026-09-18"
1552
+ },
1553
+ "aws-textract-detect-document-text-high-volume": {
1554
+ mode: "ocr",
1555
+ unit: "usd_per_page",
1556
+ rate: 6e-4,
1557
+ provider: "aws",
1558
+ source_url: "https://aws.amazon.com/textract/pricing/",
1559
+ retrieved_at: "2026-09-18"
1560
+ },
1561
+ "tavus-cvi-growth": {
1562
+ mode: "avatar",
1563
+ unit: "usd_per_minute",
1564
+ rate: 0.32,
1565
+ provider: "tavus",
1566
+ source_url: "https://www.tavus.io/pricing",
1567
+ retrieved_at: "2026-09-18"
1568
+ },
1569
+ "tavus-cvi-business": {
1570
+ mode: "avatar",
1571
+ unit: "usd_per_minute",
1572
+ rate: 0.26,
1573
+ provider: "tavus",
1574
+ source_url: "https://www.tavus.io/pricing",
1575
+ retrieved_at: "2026-09-18"
1368
1576
  }
1369
1577
  }
1370
1578
  };
1371
1579
 
1372
- // src/voice-pricing.ts
1373
- var VOICE_MAP = cost_map_default["__voice__"] ?? {};
1580
+ // src/leg-units.ts
1374
1581
  var unitForMode = {
1375
1582
  stt: "usd_per_second",
1376
1583
  tts: "usd_per_1k_chars",
1377
- telephony: "usd_per_minute"
1584
+ telephony: "usd_per_minute",
1585
+ sms: "usd_per_segment",
1586
+ ocr: "usd_per_page",
1587
+ gpu: "usd_per_gpu_second",
1588
+ avatar: "usd_per_minute"
1378
1589
  };
1590
+ var LEG_MODES = Object.keys(unitForMode);
1591
+
1592
+ // src/rate-card.ts
1593
+ var RATE_CARD_ENV = "FLOE_RATE_CARD";
1594
+ var ALLOWED_ENTRY_KEYS = /* @__PURE__ */ new Set([
1595
+ "mode",
1596
+ "unit",
1597
+ "rate",
1598
+ "provider",
1599
+ "source_url",
1600
+ "retrieved_at",
1601
+ "confirmed"
1602
+ ]);
1603
+ var REQUIRED_ENTRY_KEYS = ["mode", "unit", "rate"];
1604
+ var RATE_CARD = {};
1605
+ function envValue(name) {
1606
+ const p = globalThis.process;
1607
+ return p?.env?.[name];
1608
+ }
1609
+ function validateEntry(key, entry) {
1610
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
1611
+ throw new TypeError(`Rate card entry '${key}' must be an object.`);
1612
+ }
1613
+ const e = entry;
1614
+ const extra = Object.keys(e).filter((k) => !ALLOWED_ENTRY_KEYS.has(k)).sort();
1615
+ if (extra.length) {
1616
+ throw new TypeError(
1617
+ `Rate card entry '${key}' has unknown field(s): ${extra.join(", ")}. Allowed: ${[...ALLOWED_ENTRY_KEYS].sort().join(", ")}.`
1618
+ );
1619
+ }
1620
+ const missing = REQUIRED_ENTRY_KEYS.filter((k) => !(k in e));
1621
+ if (missing.length) {
1622
+ throw new TypeError(`Rate card entry '${key}' is missing required field(s): ${missing.join(", ")}.`);
1623
+ }
1624
+ for (const field of ["mode", "unit"]) {
1625
+ if (typeof e[field] !== "string" || !e[field]) {
1626
+ throw new TypeError(`Rate card entry '${key}': ${field} must be a non-empty string.`);
1627
+ }
1628
+ }
1629
+ const mode = e.mode;
1630
+ if (!LEG_MODES.includes(mode)) {
1631
+ throw new TypeError(
1632
+ `Rate card entry '${key}': unknown mode '${mode}'. Expected one of: ${[...LEG_MODES].sort().join(", ")}.`
1633
+ );
1634
+ }
1635
+ const expectedUnit = unitForMode[mode];
1636
+ if (e.unit !== expectedUnit) {
1637
+ throw new TypeError(
1638
+ `Rate card entry '${key}': a '${mode}' leg bills in '${expectedUnit}', got '${String(e.unit)}'. The unit is not a label \u2014 pricing multiplies by it, so the wrong one mis-bills by whatever the conversion factor is.`
1639
+ );
1640
+ }
1641
+ const rate = e.rate;
1642
+ if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0) {
1643
+ throw new RangeError(
1644
+ `Rate card entry '${key}': rate must be a finite, non-negative number, got ${String(rate)}.`
1645
+ );
1646
+ }
1647
+ if ("confirmed" in e && typeof e.confirmed !== "boolean") {
1648
+ throw new TypeError(`Rate card entry '${key}': confirmed must be true or false.`);
1649
+ }
1650
+ for (const field of ["provider", "source_url", "retrieved_at"]) {
1651
+ if (field in e && typeof e[field] !== "string") {
1652
+ throw new TypeError(`Rate card entry '${key}': ${field} must be a string.`);
1653
+ }
1654
+ }
1655
+ }
1656
+ function loadRateCard(source) {
1657
+ let input = source;
1658
+ if (input === void 0 || input === null) {
1659
+ const fromEnv = envValue(RATE_CARD_ENV);
1660
+ if (!fromEnv) return {};
1661
+ input = fromEnv;
1662
+ }
1663
+ let raw;
1664
+ if (typeof input === "string") {
1665
+ try {
1666
+ raw = JSON.parse(input);
1667
+ } catch (e) {
1668
+ throw new TypeError(
1669
+ `Rate card is not valid JSON: ${e.message}. ${RATE_CARD_ENV} holds inline JSON in this package, not a file path.`
1670
+ );
1671
+ }
1672
+ } else {
1673
+ raw = input;
1674
+ }
1675
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
1676
+ throw new TypeError("Rate card must be a JSON object mapping vendor keys to entries.");
1677
+ }
1678
+ const out = {};
1679
+ for (const [key, entry] of Object.entries(raw)) {
1680
+ validateEntry(key, entry);
1681
+ out[key] = { ...entry };
1682
+ }
1683
+ return out;
1684
+ }
1685
+ function freeze(card) {
1686
+ for (const entry of Object.values(card)) Object.freeze(entry);
1687
+ return Object.freeze(card);
1688
+ }
1689
+ function currentRateCard() {
1690
+ return RATE_CARD;
1691
+ }
1692
+ RATE_CARD = freeze(loadRateCard());
1693
+
1694
+ // src/voice-pricing.ts
1695
+ var rawCostMap = cost_map_default;
1696
+ var VOICE_MAP = rawCostMap["__legs__"] ?? rawCostMap["__voice__"] ?? {};
1379
1697
  function finiteNonNegative(value) {
1380
1698
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
1381
1699
  }
1382
- function lookupVoiceRate(model, mode) {
1700
+ function entryIsUsable(entry, mode) {
1701
+ if (typeof entry !== "object" || entry === null) return false;
1702
+ const e = entry;
1703
+ if (e.mode !== mode) return false;
1704
+ if (e.unit !== unitForMode[mode]) return false;
1705
+ return finiteNonNegative(e.rate);
1706
+ }
1707
+ function lookupLegEntry(model, mode) {
1383
1708
  if (model === null || model === void 0) return null;
1709
+ const card = currentRateCard();
1710
+ const declared = Object.prototype.hasOwnProperty.call(card, model) ? card[model] : void 0;
1711
+ if (declared && declared.mode === mode) {
1712
+ return entryIsUsable(declared, mode) ? { entry: declared, source: "rate_card" } : null;
1713
+ }
1384
1714
  const entry = Object.prototype.hasOwnProperty.call(VOICE_MAP, model) ? VOICE_MAP[model] : void 0;
1385
- if (!entry) return null;
1386
- if (entry.mode !== mode) return null;
1387
- if (entry.unit !== unitForMode[mode]) return null;
1388
- const rate = entry.rate;
1389
- if (!finiteNonNegative(rate)) return null;
1390
- return rate;
1715
+ if (entryIsUsable(entry, mode)) return { entry, source: "cost_map" };
1716
+ return null;
1391
1717
  }
1392
1718
  function resolveVoiceRate(model, mode, override) {
1393
1719
  if (override !== void 0 && override !== null) {
@@ -1396,20 +1722,46 @@ function resolveVoiceRate(model, mode, override) {
1396
1722
  `voice ${mode} override must be a finite, non-negative number, got ${override}`
1397
1723
  );
1398
1724
  }
1399
- return { mode, unit: unitForMode[mode], rate: override, source: "override" };
1725
+ return {
1726
+ mode,
1727
+ unit: unitForMode[mode],
1728
+ rate: override,
1729
+ source: "override",
1730
+ confirmed: true
1731
+ };
1400
1732
  }
1401
- const rate = lookupVoiceRate(model, mode);
1402
- if (rate === null) throw new UnpriceableLegError(model ?? null, mode);
1403
- return { mode, unit: unitForMode[mode], rate, source: "cost_map" };
1733
+ const found = lookupLegEntry(model, mode);
1734
+ if (found === null) throw new UnpriceableLegError(model ?? null, mode);
1735
+ const { entry, source } = found;
1736
+ const str = (v) => typeof v === "string" ? v : void 0;
1737
+ return {
1738
+ mode,
1739
+ unit: unitForMode[mode],
1740
+ rate: entry.rate,
1741
+ source,
1742
+ ...str(entry.provider) !== void 0 ? { provider: str(entry.provider) } : {},
1743
+ ...str(entry.source_url) !== void 0 ? { source_url: str(entry.source_url) } : {},
1744
+ ...str(entry.retrieved_at) !== void 0 ? { retrieved_at: str(entry.retrieved_at) } : {},
1745
+ // A declared rate is confirmed unless the card says otherwise (an imported or
1746
+ // draft card can mark entries unconfirmed); a bundled list price is
1747
+ // UNCONFIRMED until a human accepts it.
1748
+ confirmed: typeof entry.confirmed === "boolean" ? entry.confirmed : source === "rate_card"
1749
+ };
1404
1750
  }
1405
1751
  function voiceLegCost(mode, quantity, rate) {
1406
1752
  if (!Number.isFinite(quantity)) {
1407
1753
  throw new RangeError(`voice ${mode} quantity must be a finite number, got ${quantity}`);
1408
1754
  }
1409
1755
  const q = Math.max(0, quantity);
1410
- if (mode === "stt") return q * rate;
1756
+ if (mode === "stt" || // seconds * $/second
1757
+ mode === "telephony" || // minutes * $/minute
1758
+ mode === "sms" || // segments * $/segment
1759
+ mode === "ocr" || // pages * $/page
1760
+ mode === "gpu" || // gpu-seconds * $/gpu-second
1761
+ mode === "avatar") {
1762
+ return q * rate;
1763
+ }
1411
1764
  if (mode === "tts") return q / 1e3 * rate;
1412
- if (mode === "telephony") return q * rate;
1413
1765
  throw new RangeError(`unknown voice mode ${mode}`);
1414
1766
  }
1415
1767
  function priceVoiceLeg(mode, quantity, options = {}) {