floe-guard 0.17.0 → 0.18.1

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;
@@ -76,8 +76,8 @@ function preCall(guard, options = {}) {
76
76
  // src/cost_map.json
77
77
  var cost_map_default = {
78
78
  __meta__: {
79
- generated_at: "2026-09-14",
80
- source: "LiteLLM public model prices (bundled snapshot); voice rates verified from vendor list pages"
79
+ generated_at: "2026-09-21",
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,
@@ -621,18 +621,18 @@ var cost_map_default = {
621
621
  cache_read_input_token_cost: 3e-8
622
622
  },
623
623
  "gemini-flash-latest": {
624
- input_cost_per_token: 3e-7,
625
- output_cost_per_token: 25e-7,
624
+ input_cost_per_token: 75e-8,
625
+ output_cost_per_token: 375e-8,
626
626
  litellm_provider: "gemini",
627
627
  mode: "chat",
628
- cache_read_input_token_cost: 3e-8
628
+ cache_read_input_token_cost: 75e-9
629
629
  },
630
630
  "gemini-flash-lite-latest": {
631
- input_cost_per_token: 1e-7,
632
- output_cost_per_token: 4e-7,
631
+ input_cost_per_token: 3e-7,
632
+ output_cost_per_token: 25e-7,
633
633
  litellm_provider: "gemini",
634
634
  mode: "chat",
635
- cache_read_input_token_cost: 1e-8
635
+ cache_read_input_token_cost: 3e-8
636
636
  },
637
637
  "gemini-gemma-2-27b-it": {
638
638
  input_cost_per_token: 35e-8,
@@ -659,11 +659,11 @@ var cost_map_default = {
659
659
  mode: "chat"
660
660
  },
661
661
  "gemini-pro-latest": {
662
- input_cost_per_token: 125e-8,
663
- output_cost_per_token: 1e-5,
662
+ input_cost_per_token: 2e-6,
663
+ output_cost_per_token: 12e-6,
664
664
  litellm_provider: "gemini",
665
665
  mode: "chat",
666
- cache_read_input_token_cost: 125e-9
666
+ cache_read_input_token_cost: 2e-7
667
667
  },
668
668
  "gemini-robotics-er-1.5-preview": {
669
669
  input_cost_per_token: 3e-7,
@@ -678,11 +678,11 @@ var cost_map_default = {
678
678
  mode: "chat"
679
679
  },
680
680
  "gemini-robotics-er-2-preview": {
681
- input_cost_per_token: 2e-6,
682
- output_cost_per_token: 1e-5,
681
+ input_cost_per_token: 1e-6,
682
+ output_cost_per_token: 5e-6,
683
683
  litellm_provider: "gemini",
684
684
  mode: "chat",
685
- cache_read_input_token_cost: 2e-7
685
+ cache_read_input_token_cost: 1e-7
686
686
  },
687
687
  "gemini-robotics-er-2-streaming-preview": {
688
688
  input_cost_per_token: 2e-6,
@@ -1078,6 +1078,13 @@ var cost_map_default = {
1078
1078
  mode: "chat",
1079
1079
  cache_read_input_token_cost: 5e-7
1080
1080
  },
1081
+ "gpt-5.5-cyber": {
1082
+ input_cost_per_token: 125e-7,
1083
+ output_cost_per_token: 75e-6,
1084
+ litellm_provider: "openai",
1085
+ mode: "chat",
1086
+ cache_read_input_token_cost: 125e-8
1087
+ },
1081
1088
  "gpt-5.6": {
1082
1089
  input_cost_per_token: 4e-6,
1083
1090
  output_cost_per_token: 2e-5,
@@ -1162,6 +1169,13 @@ var cost_map_default = {
1162
1169
  litellm_provider: "openai",
1163
1170
  mode: "chat"
1164
1171
  },
1172
+ "gpt-rosalind-research": {
1173
+ input_cost_per_token: 5e-6,
1174
+ output_cost_per_token: 25e-6,
1175
+ litellm_provider: "openai",
1176
+ mode: "chat",
1177
+ cache_read_input_token_cost: 5e-7
1178
+ },
1165
1179
  "llama-3.1-8b-instant": {
1166
1180
  input_cost_per_token: 5e-8,
1167
1181
  output_cost_per_token: 8e-8,
@@ -1287,7 +1301,7 @@ var cost_map_default = {
1287
1301
  litellm_provider: "openai",
1288
1302
  mode: "embedding"
1289
1303
  },
1290
- __voice__: {
1304
+ __legs__: {
1291
1305
  "deepgram-nova-3": {
1292
1306
  mode: "stt",
1293
1307
  unit: "usd_per_second",
@@ -1365,29 +1379,355 @@ var cost_map_default = {
1365
1379
  unit: "usd_per_minute",
1366
1380
  rate: 4e-3,
1367
1381
  provider: "twilio"
1382
+ },
1383
+ "twilio-sms-us-outbound": {
1384
+ mode: "sms",
1385
+ unit: "usd_per_segment",
1386
+ rate: 83e-4,
1387
+ provider: "twilio",
1388
+ source_url: "https://www.twilio.com/en-us/sms/pricing/us",
1389
+ retrieved_at: "2026-09-18"
1390
+ },
1391
+ "twilio-sms-us-inbound": {
1392
+ mode: "sms",
1393
+ unit: "usd_per_segment",
1394
+ rate: 83e-4,
1395
+ provider: "twilio",
1396
+ source_url: "https://www.twilio.com/en-us/sms/pricing/us",
1397
+ retrieved_at: "2026-09-18"
1398
+ },
1399
+ "telnyx-sms-us-outbound": {
1400
+ mode: "sms",
1401
+ unit: "usd_per_segment",
1402
+ rate: 4e-3,
1403
+ provider: "telnyx",
1404
+ source_url: "https://telnyx.com/pricing/messaging",
1405
+ retrieved_at: "2026-09-18"
1406
+ },
1407
+ "telnyx-sms-us-inbound": {
1408
+ mode: "sms",
1409
+ unit: "usd_per_segment",
1410
+ rate: 4e-3,
1411
+ provider: "telnyx",
1412
+ source_url: "https://telnyx.com/pricing/messaging",
1413
+ retrieved_at: "2026-09-18"
1414
+ },
1415
+ "modal-b300": {
1416
+ mode: "gpu",
1417
+ unit: "usd_per_gpu_second",
1418
+ rate: 1972e-6,
1419
+ provider: "modal",
1420
+ source_url: "https://modal.com/pricing",
1421
+ retrieved_at: "2026-09-18"
1422
+ },
1423
+ "modal-b200": {
1424
+ mode: "gpu",
1425
+ unit: "usd_per_gpu_second",
1426
+ rate: 1736e-6,
1427
+ provider: "modal",
1428
+ source_url: "https://modal.com/pricing",
1429
+ retrieved_at: "2026-09-18"
1430
+ },
1431
+ "modal-h200-sxm": {
1432
+ mode: "gpu",
1433
+ unit: "usd_per_gpu_second",
1434
+ rate: 1261e-6,
1435
+ provider: "modal",
1436
+ source_url: "https://modal.com/pricing",
1437
+ retrieved_at: "2026-09-18"
1438
+ },
1439
+ "modal-h100-sxm5": {
1440
+ mode: "gpu",
1441
+ unit: "usd_per_gpu_second",
1442
+ rate: 1097e-6,
1443
+ provider: "modal",
1444
+ source_url: "https://modal.com/pricing",
1445
+ retrieved_at: "2026-09-18"
1446
+ },
1447
+ "modal-rtx-pro-6000": {
1448
+ mode: "gpu",
1449
+ unit: "usd_per_gpu_second",
1450
+ rate: 842e-6,
1451
+ provider: "modal",
1452
+ source_url: "https://modal.com/pricing",
1453
+ retrieved_at: "2026-09-18"
1454
+ },
1455
+ "modal-a100-80gb": {
1456
+ mode: "gpu",
1457
+ unit: "usd_per_gpu_second",
1458
+ rate: 694e-6,
1459
+ provider: "modal",
1460
+ source_url: "https://modal.com/pricing",
1461
+ retrieved_at: "2026-09-18"
1462
+ },
1463
+ "modal-a100-40gb": {
1464
+ mode: "gpu",
1465
+ unit: "usd_per_gpu_second",
1466
+ rate: 583e-6,
1467
+ provider: "modal",
1468
+ source_url: "https://modal.com/pricing",
1469
+ retrieved_at: "2026-09-18"
1470
+ },
1471
+ "modal-l40s": {
1472
+ mode: "gpu",
1473
+ unit: "usd_per_gpu_second",
1474
+ rate: 542e-6,
1475
+ provider: "modal",
1476
+ source_url: "https://modal.com/pricing",
1477
+ retrieved_at: "2026-09-18"
1478
+ },
1479
+ "modal-a10": {
1480
+ mode: "gpu",
1481
+ unit: "usd_per_gpu_second",
1482
+ rate: 306e-6,
1483
+ provider: "modal",
1484
+ source_url: "https://modal.com/pricing",
1485
+ retrieved_at: "2026-09-18"
1486
+ },
1487
+ "modal-l4": {
1488
+ mode: "gpu",
1489
+ unit: "usd_per_gpu_second",
1490
+ rate: 222e-6,
1491
+ provider: "modal",
1492
+ source_url: "https://modal.com/pricing",
1493
+ retrieved_at: "2026-09-18"
1494
+ },
1495
+ "modal-t4": {
1496
+ mode: "gpu",
1497
+ unit: "usd_per_gpu_second",
1498
+ rate: 164e-6,
1499
+ provider: "modal",
1500
+ source_url: "https://modal.com/pricing",
1501
+ retrieved_at: "2026-09-18"
1502
+ },
1503
+ "gcp-vision-text-detection": {
1504
+ mode: "ocr",
1505
+ unit: "usd_per_page",
1506
+ rate: 15e-4,
1507
+ provider: "gcp",
1508
+ source_url: "https://cloud.google.com/vision/pricing",
1509
+ retrieved_at: "2026-09-18"
1510
+ },
1511
+ "gcp-vision-document-text-detection": {
1512
+ mode: "ocr",
1513
+ unit: "usd_per_page",
1514
+ rate: 15e-4,
1515
+ provider: "gcp",
1516
+ source_url: "https://cloud.google.com/vision/pricing",
1517
+ retrieved_at: "2026-09-18"
1518
+ },
1519
+ "gcp-vision-text-detection-high-volume": {
1520
+ mode: "ocr",
1521
+ unit: "usd_per_page",
1522
+ rate: 6e-4,
1523
+ provider: "gcp",
1524
+ source_url: "https://cloud.google.com/vision/pricing",
1525
+ retrieved_at: "2026-09-18"
1526
+ },
1527
+ "gcp-vision-document-text-detection-high-volume": {
1528
+ mode: "ocr",
1529
+ unit: "usd_per_page",
1530
+ rate: 6e-4,
1531
+ provider: "gcp",
1532
+ source_url: "https://cloud.google.com/vision/pricing",
1533
+ retrieved_at: "2026-09-18"
1534
+ },
1535
+ "aws-textract-detect-document-text": {
1536
+ mode: "ocr",
1537
+ unit: "usd_per_page",
1538
+ rate: 15e-4,
1539
+ provider: "aws",
1540
+ source_url: "https://aws.amazon.com/textract/pricing/",
1541
+ retrieved_at: "2026-09-18"
1542
+ },
1543
+ "aws-textract-detect-document-text-high-volume": {
1544
+ mode: "ocr",
1545
+ unit: "usd_per_page",
1546
+ rate: 6e-4,
1547
+ provider: "aws",
1548
+ source_url: "https://aws.amazon.com/textract/pricing/",
1549
+ retrieved_at: "2026-09-18"
1550
+ },
1551
+ "aws-textract-analyze-forms": {
1552
+ mode: "ocr",
1553
+ unit: "usd_per_page",
1554
+ rate: 0.05,
1555
+ provider: "aws",
1556
+ source_url: "https://aws.amazon.com/textract/pricing/",
1557
+ retrieved_at: "2026-09-18"
1558
+ },
1559
+ "aws-textract-analyze-tables": {
1560
+ mode: "ocr",
1561
+ unit: "usd_per_page",
1562
+ rate: 0.015,
1563
+ provider: "aws",
1564
+ source_url: "https://aws.amazon.com/textract/pricing/",
1565
+ retrieved_at: "2026-09-18"
1566
+ },
1567
+ "tavus-cvi-starter": {
1568
+ mode: "avatar",
1569
+ unit: "usd_per_minute",
1570
+ rate: 0.37,
1571
+ provider: "tavus",
1572
+ source_url: "https://www.tavus.io/pricing",
1573
+ retrieved_at: "2026-09-18"
1574
+ },
1575
+ "tavus-cvi-growth": {
1576
+ mode: "avatar",
1577
+ unit: "usd_per_minute",
1578
+ rate: 0.32,
1579
+ provider: "tavus",
1580
+ source_url: "https://www.tavus.io/pricing",
1581
+ retrieved_at: "2026-09-18"
1582
+ },
1583
+ "tavus-cvi-business": {
1584
+ mode: "avatar",
1585
+ unit: "usd_per_minute",
1586
+ rate: 0.26,
1587
+ provider: "tavus",
1588
+ source_url: "https://www.tavus.io/pricing",
1589
+ retrieved_at: "2026-09-18"
1368
1590
  }
1369
1591
  }
1370
1592
  };
1371
1593
 
1372
- // src/voice-pricing.ts
1373
- var VOICE_MAP = cost_map_default["__voice__"] ?? {};
1594
+ // src/leg-units.ts
1374
1595
  var unitForMode = {
1375
1596
  stt: "usd_per_second",
1376
1597
  tts: "usd_per_1k_chars",
1377
- telephony: "usd_per_minute"
1598
+ telephony: "usd_per_minute",
1599
+ sms: "usd_per_segment",
1600
+ ocr: "usd_per_page",
1601
+ gpu: "usd_per_gpu_second",
1602
+ avatar: "usd_per_minute"
1378
1603
  };
1604
+ var LEG_MODES = Object.keys(unitForMode);
1605
+
1606
+ // src/rate-card.ts
1607
+ var RATE_CARD_ENV = "FLOE_RATE_CARD";
1608
+ var ALLOWED_ENTRY_KEYS = /* @__PURE__ */ new Set([
1609
+ "mode",
1610
+ "unit",
1611
+ "rate",
1612
+ "provider",
1613
+ "source_url",
1614
+ "retrieved_at",
1615
+ "confirmed"
1616
+ ]);
1617
+ var REQUIRED_ENTRY_KEYS = ["mode", "unit", "rate"];
1618
+ var RATE_CARD = {};
1619
+ function envValue(name) {
1620
+ const p = globalThis.process;
1621
+ return p?.env?.[name];
1622
+ }
1623
+ function validateEntry(key, entry) {
1624
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
1625
+ throw new TypeError(`Rate card entry '${key}' must be an object.`);
1626
+ }
1627
+ const e = entry;
1628
+ const extra = Object.keys(e).filter((k) => !ALLOWED_ENTRY_KEYS.has(k)).sort();
1629
+ if (extra.length) {
1630
+ throw new TypeError(
1631
+ `Rate card entry '${key}' has unknown field(s): ${extra.join(", ")}. Allowed: ${[...ALLOWED_ENTRY_KEYS].sort().join(", ")}.`
1632
+ );
1633
+ }
1634
+ const missing = REQUIRED_ENTRY_KEYS.filter((k) => !(k in e));
1635
+ if (missing.length) {
1636
+ throw new TypeError(`Rate card entry '${key}' is missing required field(s): ${missing.join(", ")}.`);
1637
+ }
1638
+ for (const field of ["mode", "unit"]) {
1639
+ if (typeof e[field] !== "string" || !e[field]) {
1640
+ throw new TypeError(`Rate card entry '${key}': ${field} must be a non-empty string.`);
1641
+ }
1642
+ }
1643
+ const mode = e.mode;
1644
+ if (!LEG_MODES.includes(mode)) {
1645
+ throw new TypeError(
1646
+ `Rate card entry '${key}': unknown mode '${mode}'. Expected one of: ${[...LEG_MODES].sort().join(", ")}.`
1647
+ );
1648
+ }
1649
+ const expectedUnit = unitForMode[mode];
1650
+ if (e.unit !== expectedUnit) {
1651
+ throw new TypeError(
1652
+ `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.`
1653
+ );
1654
+ }
1655
+ const rate = e.rate;
1656
+ if (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0) {
1657
+ throw new RangeError(
1658
+ `Rate card entry '${key}': rate must be a finite, non-negative number, got ${String(rate)}.`
1659
+ );
1660
+ }
1661
+ if ("confirmed" in e && typeof e.confirmed !== "boolean") {
1662
+ throw new TypeError(`Rate card entry '${key}': confirmed must be true or false.`);
1663
+ }
1664
+ for (const field of ["provider", "source_url", "retrieved_at"]) {
1665
+ if (field in e && typeof e[field] !== "string") {
1666
+ throw new TypeError(`Rate card entry '${key}': ${field} must be a string.`);
1667
+ }
1668
+ }
1669
+ }
1670
+ function loadRateCard(source) {
1671
+ let input = source;
1672
+ if (input === void 0 || input === null) {
1673
+ const fromEnv = envValue(RATE_CARD_ENV);
1674
+ if (!fromEnv) return {};
1675
+ input = fromEnv;
1676
+ }
1677
+ let raw;
1678
+ if (typeof input === "string") {
1679
+ try {
1680
+ raw = JSON.parse(input);
1681
+ } catch (e) {
1682
+ throw new TypeError(
1683
+ `Rate card is not valid JSON: ${e.message}. ${RATE_CARD_ENV} holds inline JSON in this package, not a file path.`
1684
+ );
1685
+ }
1686
+ } else {
1687
+ raw = input;
1688
+ }
1689
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
1690
+ throw new TypeError("Rate card must be a JSON object mapping vendor keys to entries.");
1691
+ }
1692
+ const out = {};
1693
+ for (const [key, entry] of Object.entries(raw)) {
1694
+ validateEntry(key, entry);
1695
+ out[key] = { ...entry };
1696
+ }
1697
+ return out;
1698
+ }
1699
+ function freeze(card) {
1700
+ for (const entry of Object.values(card)) Object.freeze(entry);
1701
+ return Object.freeze(card);
1702
+ }
1703
+ function currentRateCard() {
1704
+ return RATE_CARD;
1705
+ }
1706
+ RATE_CARD = freeze(loadRateCard());
1707
+
1708
+ // src/voice-pricing.ts
1709
+ var rawCostMap = cost_map_default;
1710
+ var VOICE_MAP = rawCostMap["__legs__"] ?? rawCostMap["__voice__"] ?? {};
1379
1711
  function finiteNonNegative(value) {
1380
1712
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
1381
1713
  }
1382
- function lookupVoiceRate(model, mode) {
1714
+ function entryIsUsable(entry, mode) {
1715
+ if (typeof entry !== "object" || entry === null) return false;
1716
+ const e = entry;
1717
+ if (e.mode !== mode) return false;
1718
+ if (e.unit !== unitForMode[mode]) return false;
1719
+ return finiteNonNegative(e.rate);
1720
+ }
1721
+ function lookupLegEntry(model, mode) {
1383
1722
  if (model === null || model === void 0) return null;
1723
+ const card = currentRateCard();
1724
+ const declared = Object.prototype.hasOwnProperty.call(card, model) ? card[model] : void 0;
1725
+ if (declared && declared.mode === mode) {
1726
+ return entryIsUsable(declared, mode) ? { entry: declared, source: "rate_card" } : null;
1727
+ }
1384
1728
  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;
1729
+ if (entryIsUsable(entry, mode)) return { entry, source: "cost_map" };
1730
+ return null;
1391
1731
  }
1392
1732
  function resolveVoiceRate(model, mode, override) {
1393
1733
  if (override !== void 0 && override !== null) {
@@ -1396,20 +1736,46 @@ function resolveVoiceRate(model, mode, override) {
1396
1736
  `voice ${mode} override must be a finite, non-negative number, got ${override}`
1397
1737
  );
1398
1738
  }
1399
- return { mode, unit: unitForMode[mode], rate: override, source: "override" };
1739
+ return {
1740
+ mode,
1741
+ unit: unitForMode[mode],
1742
+ rate: override,
1743
+ source: "override",
1744
+ confirmed: true
1745
+ };
1400
1746
  }
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" };
1747
+ const found = lookupLegEntry(model, mode);
1748
+ if (found === null) throw new UnpriceableLegError(model ?? null, mode);
1749
+ const { entry, source } = found;
1750
+ const str = (v) => typeof v === "string" ? v : void 0;
1751
+ return {
1752
+ mode,
1753
+ unit: unitForMode[mode],
1754
+ rate: entry.rate,
1755
+ source,
1756
+ ...str(entry.provider) !== void 0 ? { provider: str(entry.provider) } : {},
1757
+ ...str(entry.source_url) !== void 0 ? { source_url: str(entry.source_url) } : {},
1758
+ ...str(entry.retrieved_at) !== void 0 ? { retrieved_at: str(entry.retrieved_at) } : {},
1759
+ // A declared rate is confirmed unless the card says otherwise (an imported or
1760
+ // draft card can mark entries unconfirmed); a bundled list price is
1761
+ // UNCONFIRMED until a human accepts it.
1762
+ confirmed: typeof entry.confirmed === "boolean" ? entry.confirmed : source === "rate_card"
1763
+ };
1404
1764
  }
1405
1765
  function voiceLegCost(mode, quantity, rate) {
1406
1766
  if (!Number.isFinite(quantity)) {
1407
1767
  throw new RangeError(`voice ${mode} quantity must be a finite number, got ${quantity}`);
1408
1768
  }
1409
1769
  const q = Math.max(0, quantity);
1410
- if (mode === "stt") return q * rate;
1770
+ if (mode === "stt" || // seconds * $/second
1771
+ mode === "telephony" || // minutes * $/minute
1772
+ mode === "sms" || // segments * $/segment
1773
+ mode === "ocr" || // pages * $/page
1774
+ mode === "gpu" || // gpu-seconds * $/gpu-second
1775
+ mode === "avatar") {
1776
+ return q * rate;
1777
+ }
1411
1778
  if (mode === "tts") return q / 1e3 * rate;
1412
- if (mode === "telephony") return q * rate;
1413
1779
  throw new RangeError(`unknown voice mode ${mode}`);
1414
1780
  }
1415
1781
  function priceVoiceLeg(mode, quantity, options = {}) {