floe-guard 0.15.7 → 0.16.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/README.md +73 -0
- package/dist/adapters/livekit.d.cts +1 -1
- package/dist/adapters/livekit.d.ts +1 -1
- package/dist/adapters/retell.d.cts +1 -1
- package/dist/adapters/retell.d.ts +1 -1
- package/dist/adapters/vapi.d.cts +2 -2
- package/dist/adapters/vapi.d.ts +2 -2
- package/dist/{guard-BPIqdSVt.d.cts → guard-Bbs5OxAF.d.cts} +17 -1
- package/dist/{guard-BPIqdSVt.d.ts → guard-Bbs5OxAF.d.ts} +17 -1
- package/dist/index.cjs +171 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -3
- package/dist/index.d.ts +68 -3
- package/dist/index.js +168 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,6 +34,79 @@ The middleware sits in the call path: it `check()`s before `doGenerate` /
|
|
|
34
34
|
`doStream` (throwing `BudgetExceeded` to halt the run) and `record()`s priced
|
|
35
35
|
token usage after — for streaming it reads usage from the `finish` part.
|
|
36
36
|
|
|
37
|
+
## Mid-stream budget enforcement
|
|
38
|
+
|
|
39
|
+
`StreamGuard` and `guardStream` port Python's streaming USD guard. They price
|
|
40
|
+
each text delta before it reaches the consumer. When a chunk crosses the
|
|
41
|
+
shared ceiling, its partial spend is recorded in `guard.spendLog` **before**
|
|
42
|
+
`BudgetExceeded` is thrown. Active streams on the same guard share the ceiling.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { BudgetGuard, guardStream } from "floe-guard";
|
|
46
|
+
|
|
47
|
+
const guard = new BudgetGuard(0.01);
|
|
48
|
+
// textDeltas is your provider's Iterable<string> or AsyncIterable<string>.
|
|
49
|
+
for await (const text of guardStream(guard, "gpt-4o", textDeltas)) {
|
|
50
|
+
consume(text);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
For structured chunks, supply `{ getText: chunk => chunk.delta.text ?? "" }`
|
|
55
|
+
using your provider's actual shape. Without an extractor, non-string chunks
|
|
56
|
+
throw instead of silently recording zero. Synchronous inputs return a
|
|
57
|
+
synchronous iterator; asynchronous inputs return an asynchronous iterator.
|
|
58
|
+
|
|
59
|
+
Use `StreamGuard` directly when the provider reports final usage:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { StreamGuard } from "floe-guard";
|
|
63
|
+
|
|
64
|
+
const reserved = guard.reserve(guard.estimateCall("gpt-4o", 100, 200));
|
|
65
|
+
const stream = new StreamGuard(guard, "gpt-4o", { promptTokens: 100, reserved });
|
|
66
|
+
try {
|
|
67
|
+
for await (const text of textDeltas) {
|
|
68
|
+
stream.feedText(text); // or feedTokens(n) with known per-chunk counts
|
|
69
|
+
consume(text);
|
|
70
|
+
}
|
|
71
|
+
stream.finish({ promptTokens: reportedPromptTokens, completionTokens: reportedCompletionTokens });
|
|
72
|
+
} finally {
|
|
73
|
+
stream.close(); // idempotent; settles estimates if finish() was not reached
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- Options: `promptTokens`, `reserved`, `price`, `label`, and `countTokens(delta)`.
|
|
78
|
+
`guardStream` additionally accepts `getText(chunk)`.
|
|
79
|
+
Configuration is captured at construction; changing the caller's options or
|
|
80
|
+
manual price object later does not change an existing stream.
|
|
81
|
+
- `approxTokens` defaults to roughly four Unicode characters per token, with
|
|
82
|
+
a minimum of one for a non-empty delta. A custom tokenizer can replace it.
|
|
83
|
+
`finish()` reconciles to reported usage; `completionTokens` exposes the running
|
|
84
|
+
estimate. Mid-stream checks cover the aggregate USD ceiling, matching Python;
|
|
85
|
+
token reservations are reconciled at settlement.
|
|
86
|
+
- An unpriceable model fails at construction and releases its reservation.
|
|
87
|
+
With `failClosed: false`, unpriceable streams pass through and settlement
|
|
88
|
+
warns and skips accounting, matching the existing guard policy.
|
|
89
|
+
- The wrapper settles on exhaustion, source/consumer errors, and early `break`,
|
|
90
|
+
and closes the source iterator when iteration ends early. Direct users must
|
|
91
|
+
call `close()` in `finally`. If a wrapper is **never iterated**, its reservation
|
|
92
|
+
remains the caller's responsibility: call `guard.release(reserved)`.
|
|
93
|
+
- The crossing chunk has already been generated. With accurate counts, the
|
|
94
|
+
local cutoff can overshoot by that chunk; heuristic error, parallel streams,
|
|
95
|
+
provider buffering and delayed cancellation can cause additional actual
|
|
96
|
+
spend. Stopping iteration requests iterator cleanup, not guaranteed remote
|
|
97
|
+
cancellation. Connect cleanup to your provider's abort mechanism where needed.
|
|
98
|
+
- Existing Vapi, LiveKit and middleware streaming behavior is unchanged; this
|
|
99
|
+
is the standalone primitive requested in issue #124.
|
|
100
|
+
|
|
101
|
+
Run the no-key example from a repository checkout:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
cd js
|
|
105
|
+
npm ci
|
|
106
|
+
npm run build
|
|
107
|
+
node ../examples/streaming_guard.mjs
|
|
108
|
+
```
|
|
109
|
+
|
|
37
110
|
## Pricing
|
|
38
111
|
|
|
39
112
|
Tokens are priced **offline** from a bundled
|
package/dist/adapters/vapi.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { B as BudgetGuard, F as FloeGuardError } from '../guard-
|
|
2
|
-
export { a as BudgetExceeded } from '../guard-
|
|
1
|
+
import { B as BudgetGuard, F as FloeGuardError } from '../guard-Bbs5OxAF.cjs';
|
|
2
|
+
export { a as BudgetExceeded } from '../guard-Bbs5OxAF.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Vapi custom-LLM adapter (no SDK dependency — typed structurally).
|
package/dist/adapters/vapi.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { B as BudgetGuard, F as FloeGuardError } from '../guard-
|
|
2
|
-
export { a as BudgetExceeded } from '../guard-
|
|
1
|
+
import { B as BudgetGuard, F as FloeGuardError } from '../guard-Bbs5OxAF.js';
|
|
2
|
+
export { a as BudgetExceeded } from '../guard-Bbs5OxAF.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Vapi custom-LLM adapter (no SDK dependency — typed structurally).
|
|
@@ -347,6 +347,7 @@ declare class BudgetGuard {
|
|
|
347
347
|
private lastToolCost;
|
|
348
348
|
/** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */
|
|
349
349
|
private reserved;
|
|
350
|
+
private readonly streamCosts;
|
|
350
351
|
/** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */
|
|
351
352
|
private readonly spendEvents;
|
|
352
353
|
private readonly maxLogEvents?;
|
|
@@ -506,7 +507,7 @@ declare class BudgetGuard {
|
|
|
506
507
|
* before producing usage). Safe to call with `0`.
|
|
507
508
|
*/
|
|
508
509
|
release(reserved: ReservationHandle): void;
|
|
509
|
-
/** USD left
|
|
510
|
+
/** USD left, net of reservations and unsettled stream overages (never negative). */
|
|
510
511
|
get remainingUsd(): number;
|
|
511
512
|
/**
|
|
512
513
|
* Per-tool running USD totals, keyed by the name given to `settleTool()` /
|
|
@@ -624,6 +625,21 @@ declare class BudgetGuard {
|
|
|
624
625
|
* (called outside the lock) reads no shared state and stays race-free.
|
|
625
626
|
*/
|
|
626
627
|
private blockingCross;
|
|
628
|
+
/** Accrued stream costs beyond existing holds, excluding a stream if requested. */
|
|
629
|
+
private streamOverage;
|
|
630
|
+
/** @internal Validate before transferring a reservation to a stream. */
|
|
631
|
+
_validateStreamReservation(reserved: ReservationHandle): void;
|
|
632
|
+
/** @internal Register accrued-but-unsettled streaming spend. */
|
|
633
|
+
_registerStream(reserved: ReservationHandle): symbol;
|
|
634
|
+
/** @internal Settlement moved this stream's accrual into spentUsd. */
|
|
635
|
+
_unregisterStream(key: symbol): void;
|
|
636
|
+
/** @internal Replace this stream's estimate with its cumulative actual estimate.
|
|
637
|
+
* Count other streams' overages once, in addition to their existing holds.
|
|
638
|
+
* Synchronous within one JS isolate, matching Python's locked registry.
|
|
639
|
+
*/
|
|
640
|
+
_streamWouldCross(key: symbol, cumulative: number): boolean;
|
|
641
|
+
/** @internal Notify and throw after a stream has settled its partial spend. */
|
|
642
|
+
_blockStream(): never;
|
|
627
643
|
/** Notify + throw the right error for a [dimension, scope, spent, limit] block. */
|
|
628
644
|
private raiseBlock;
|
|
629
645
|
/**
|
|
@@ -347,6 +347,7 @@ declare class BudgetGuard {
|
|
|
347
347
|
private lastToolCost;
|
|
348
348
|
/** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */
|
|
349
349
|
private reserved;
|
|
350
|
+
private readonly streamCosts;
|
|
350
351
|
/** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */
|
|
351
352
|
private readonly spendEvents;
|
|
352
353
|
private readonly maxLogEvents?;
|
|
@@ -506,7 +507,7 @@ declare class BudgetGuard {
|
|
|
506
507
|
* before producing usage). Safe to call with `0`.
|
|
507
508
|
*/
|
|
508
509
|
release(reserved: ReservationHandle): void;
|
|
509
|
-
/** USD left
|
|
510
|
+
/** USD left, net of reservations and unsettled stream overages (never negative). */
|
|
510
511
|
get remainingUsd(): number;
|
|
511
512
|
/**
|
|
512
513
|
* Per-tool running USD totals, keyed by the name given to `settleTool()` /
|
|
@@ -624,6 +625,21 @@ declare class BudgetGuard {
|
|
|
624
625
|
* (called outside the lock) reads no shared state and stays race-free.
|
|
625
626
|
*/
|
|
626
627
|
private blockingCross;
|
|
628
|
+
/** Accrued stream costs beyond existing holds, excluding a stream if requested. */
|
|
629
|
+
private streamOverage;
|
|
630
|
+
/** @internal Validate before transferring a reservation to a stream. */
|
|
631
|
+
_validateStreamReservation(reserved: ReservationHandle): void;
|
|
632
|
+
/** @internal Register accrued-but-unsettled streaming spend. */
|
|
633
|
+
_registerStream(reserved: ReservationHandle): symbol;
|
|
634
|
+
/** @internal Settlement moved this stream's accrual into spentUsd. */
|
|
635
|
+
_unregisterStream(key: symbol): void;
|
|
636
|
+
/** @internal Replace this stream's estimate with its cumulative actual estimate.
|
|
637
|
+
* Count other streams' overages once, in addition to their existing holds.
|
|
638
|
+
* Synchronous within one JS isolate, matching Python's locked registry.
|
|
639
|
+
*/
|
|
640
|
+
_streamWouldCross(key: symbol, cumulative: number): boolean;
|
|
641
|
+
/** @internal Notify and throw after a stream has settled its partial spend. */
|
|
642
|
+
_blockStream(): never;
|
|
627
643
|
/** Notify + throw the right error for a [dimension, scope, spent, limit] block. */
|
|
628
644
|
private raiseBlock;
|
|
629
645
|
/**
|
package/dist/index.cjs
CHANGED
|
@@ -26,12 +26,15 @@ __export(index_exports, {
|
|
|
26
26
|
FloeGuardError: () => FloeGuardError,
|
|
27
27
|
LatencyBudget: () => LatencyBudget,
|
|
28
28
|
LedgerSyncError: () => LedgerSyncError,
|
|
29
|
+
StreamGuard: () => StreamGuard,
|
|
29
30
|
TokenBudgetExceeded: () => TokenBudgetExceeded,
|
|
30
31
|
UnpriceableModelError: () => UnpriceableModelError,
|
|
31
32
|
UnpriceableVoiceError: () => UnpriceableVoiceError,
|
|
33
|
+
approxTokens: () => approxTokens,
|
|
32
34
|
budgetGuardMiddleware: () => budgetGuardMiddleware,
|
|
33
35
|
costMapGeneratedAt: () => costMapGeneratedAt,
|
|
34
36
|
gates: () => gates_exports,
|
|
37
|
+
guardStream: () => guardStream,
|
|
35
38
|
lookupVoiceRate: () => lookupVoiceRate,
|
|
36
39
|
priceTokens: () => priceTokens,
|
|
37
40
|
priceVoiceLeg: () => priceVoiceLeg,
|
|
@@ -1736,6 +1739,7 @@ var BudgetGuard = class {
|
|
|
1736
1739
|
lastToolCost = 0;
|
|
1737
1740
|
/** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */
|
|
1738
1741
|
reserved = 0;
|
|
1742
|
+
streamCosts = /* @__PURE__ */ new Map();
|
|
1739
1743
|
/** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */
|
|
1740
1744
|
spendEvents = [];
|
|
1741
1745
|
maxLogEvents;
|
|
@@ -2056,9 +2060,9 @@ var BudgetGuard = class {
|
|
|
2056
2060
|
if (!reserved) return;
|
|
2057
2061
|
this.consumeReservation(reserved);
|
|
2058
2062
|
}
|
|
2059
|
-
/** USD left
|
|
2063
|
+
/** USD left, net of reservations and unsettled stream overages (never negative). */
|
|
2060
2064
|
get remainingUsd() {
|
|
2061
|
-
return Math.max(0, this.limitUsd - this.spentUsd - this.reserved);
|
|
2065
|
+
return Math.max(0, this.limitUsd - this.spentUsd - this.reserved - this.streamOverage());
|
|
2062
2066
|
}
|
|
2063
2067
|
/**
|
|
2064
2068
|
* Per-tool running USD totals, keyed by the name given to `settleTool()` /
|
|
@@ -2261,7 +2265,7 @@ var BudgetGuard = class {
|
|
|
2261
2265
|
* (called outside the lock) reads no shared state and stays race-free.
|
|
2262
2266
|
*/
|
|
2263
2267
|
blockingCross(estimateUsd, estimateTokens) {
|
|
2264
|
-
const committed = this.spentUsd + this.reserved;
|
|
2268
|
+
const committed = this.spentUsd + this.reserved + this.streamOverage();
|
|
2265
2269
|
if (committed > this.limitUsd - EPS || committed + estimateUsd > this.limitUsd + EPS) {
|
|
2266
2270
|
return ["usd", "aggregate", this.spentUsd, this.limitUsd];
|
|
2267
2271
|
}
|
|
@@ -2288,6 +2292,43 @@ var BudgetGuard = class {
|
|
|
2288
2292
|
}
|
|
2289
2293
|
return null;
|
|
2290
2294
|
}
|
|
2295
|
+
/** Accrued stream costs beyond existing holds, excluding a stream if requested. */
|
|
2296
|
+
streamOverage(exclude) {
|
|
2297
|
+
let overage = 0;
|
|
2298
|
+
for (const [key, stream] of this.streamCosts) {
|
|
2299
|
+
if (key !== exclude) overage += Math.max(0, stream.accrued - stream.held);
|
|
2300
|
+
}
|
|
2301
|
+
return overage;
|
|
2302
|
+
}
|
|
2303
|
+
/** @internal Validate before transferring a reservation to a stream. */
|
|
2304
|
+
_validateStreamReservation(reserved) {
|
|
2305
|
+
this.reservedUsdOf(reserved);
|
|
2306
|
+
}
|
|
2307
|
+
/** @internal Register accrued-but-unsettled streaming spend. */
|
|
2308
|
+
_registerStream(reserved) {
|
|
2309
|
+
const held = this.reservedUsdOf(reserved);
|
|
2310
|
+
const key = /* @__PURE__ */ Symbol();
|
|
2311
|
+
this.streamCosts.set(key, { accrued: 0, held });
|
|
2312
|
+
return key;
|
|
2313
|
+
}
|
|
2314
|
+
/** @internal Settlement moved this stream's accrual into spentUsd. */
|
|
2315
|
+
_unregisterStream(key) {
|
|
2316
|
+
this.streamCosts.delete(key);
|
|
2317
|
+
}
|
|
2318
|
+
/** @internal Replace this stream's estimate with its cumulative actual estimate.
|
|
2319
|
+
* Count other streams' overages once, in addition to their existing holds.
|
|
2320
|
+
* Synchronous within one JS isolate, matching Python's locked registry.
|
|
2321
|
+
*/
|
|
2322
|
+
_streamWouldCross(key, cumulative) {
|
|
2323
|
+
const own = this.streamCosts.get(key);
|
|
2324
|
+
own.accrued = cumulative;
|
|
2325
|
+
const others = this.spentUsd + Math.max(0, this.reserved - own.held) + this.streamOverage(key);
|
|
2326
|
+
return others + cumulative > this.limitUsd + EPS;
|
|
2327
|
+
}
|
|
2328
|
+
/** @internal Notify and throw after a stream has settled its partial spend. */
|
|
2329
|
+
_blockStream() {
|
|
2330
|
+
return this.raiseBlock(["usd", "aggregate", this.spentUsd, this.limitUsd]);
|
|
2331
|
+
}
|
|
2291
2332
|
/** Notify + throw the right error for a [dimension, scope, spent, limit] block. */
|
|
2292
2333
|
raiseBlock(blocked) {
|
|
2293
2334
|
const [dimension, scope, spent, limit] = blocked;
|
|
@@ -2635,6 +2676,130 @@ async function nextPlan(guard, error, current, onDegrade) {
|
|
|
2635
2676
|
return current;
|
|
2636
2677
|
}
|
|
2637
2678
|
|
|
2679
|
+
// src/stream.ts
|
|
2680
|
+
function approxTokens(text) {
|
|
2681
|
+
if (typeof text !== "string") throw new TypeError("stream text must be a string");
|
|
2682
|
+
return text ? Math.max(1, Math.floor(Array.from(text).length / 4)) : 0;
|
|
2683
|
+
}
|
|
2684
|
+
function tokenCount(value) {
|
|
2685
|
+
if (!Number.isFinite(value)) throw new RangeError("token count must be finite");
|
|
2686
|
+
return Math.max(0, Math.trunc(value));
|
|
2687
|
+
}
|
|
2688
|
+
var StreamGuard = class {
|
|
2689
|
+
/** Capture pricing and release the supplied reservation if validation fails. */
|
|
2690
|
+
constructor(guard, model, options = {}) {
|
|
2691
|
+
this.guard = guard;
|
|
2692
|
+
this.model = model;
|
|
2693
|
+
this.reserved = options.reserved ?? 0;
|
|
2694
|
+
guard._validateStreamReservation(this.reserved);
|
|
2695
|
+
try {
|
|
2696
|
+
this.price = options.price === void 0 ? void 0 : { ...options.price };
|
|
2697
|
+
this.label = options.label;
|
|
2698
|
+
this.countTokens = options.countTokens ?? approxTokens;
|
|
2699
|
+
this.promptTokens = tokenCount(options.promptTokens ?? 0);
|
|
2700
|
+
this.priced = resolvePrice(
|
|
2701
|
+
model,
|
|
2702
|
+
this.price === void 0 ? guard.priceOverrides : { ...guard.priceOverrides, [model]: this.price }
|
|
2703
|
+
);
|
|
2704
|
+
if (this.priced === null && guard.failClosed) {
|
|
2705
|
+
console.warn(`Cannot price model '${model}': pass a price override to enforce a streaming budget.`);
|
|
2706
|
+
throw new UnpriceableModelError(model);
|
|
2707
|
+
}
|
|
2708
|
+
} catch (error) {
|
|
2709
|
+
guard.release(this.reserved);
|
|
2710
|
+
throw error;
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
guard;
|
|
2714
|
+
model;
|
|
2715
|
+
tokens = 0;
|
|
2716
|
+
closed = false;
|
|
2717
|
+
priced;
|
|
2718
|
+
key;
|
|
2719
|
+
promptTokens;
|
|
2720
|
+
reserved;
|
|
2721
|
+
price;
|
|
2722
|
+
label;
|
|
2723
|
+
countTokens;
|
|
2724
|
+
/** Generated completion tokens counted so far (estimated until finish()). */
|
|
2725
|
+
get completionTokens() {
|
|
2726
|
+
return this.tokens;
|
|
2727
|
+
}
|
|
2728
|
+
/** Meter a text delta with the configured tokenizer. */
|
|
2729
|
+
feedText(delta) {
|
|
2730
|
+
if (typeof delta !== "string") throw new TypeError("stream text must be a string");
|
|
2731
|
+
const count = this.countTokens;
|
|
2732
|
+
this.feedTokens(count(delta));
|
|
2733
|
+
}
|
|
2734
|
+
/** Meter additional completion tokens, settling before a budget interruption. */
|
|
2735
|
+
feedTokens(tokens) {
|
|
2736
|
+
if (this.closed) throw new Error("stream already settled");
|
|
2737
|
+
this.tokens = tokenCount(this.tokens + tokenCount(tokens));
|
|
2738
|
+
if (this.priced === null) return;
|
|
2739
|
+
const cost = priceTokens(this.priced, this.promptTokens, this.tokens);
|
|
2740
|
+
this.key ??= this.guard._registerStream(this.reserved);
|
|
2741
|
+
if (this.guard._streamWouldCross(this.key, cost)) {
|
|
2742
|
+
this.finish();
|
|
2743
|
+
this.guard._blockStream();
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
/** Reconcile estimates to provider usage, or settle accumulated estimates. */
|
|
2747
|
+
finish(usage = {}) {
|
|
2748
|
+
if (this.closed) throw new Error("stream already settled");
|
|
2749
|
+
const prompt = tokenCount(usage.promptTokens ?? this.promptTokens);
|
|
2750
|
+
const completion = tokenCount(usage.completionTokens ?? this.tokens);
|
|
2751
|
+
this.promptTokens = prompt;
|
|
2752
|
+
this.tokens = completion;
|
|
2753
|
+
this.closed = true;
|
|
2754
|
+
try {
|
|
2755
|
+
if (this.priced === null) {
|
|
2756
|
+
this.guard.release(this.reserved);
|
|
2757
|
+
console.warn(`Cannot price model '${this.model}' at stream construction; skipping streaming spend.`);
|
|
2758
|
+
return 0;
|
|
2759
|
+
}
|
|
2760
|
+
return this.guard.settle(this.model, prompt, completion, {
|
|
2761
|
+
reserved: this.reserved,
|
|
2762
|
+
price: this.priced,
|
|
2763
|
+
label: this.label
|
|
2764
|
+
});
|
|
2765
|
+
} finally {
|
|
2766
|
+
if (this.key !== void 0) this.guard._unregisterStream(this.key);
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
/** Idempotent cleanup for a finally block; partial generated usage is billed. */
|
|
2770
|
+
close() {
|
|
2771
|
+
if (!this.closed) this.finish();
|
|
2772
|
+
}
|
|
2773
|
+
};
|
|
2774
|
+
function guardStream(guard, model, chunks, options = {}) {
|
|
2775
|
+
const stream = new StreamGuard(guard, model, options);
|
|
2776
|
+
const extract = options.getText ?? ((chunk) => {
|
|
2777
|
+
if (typeof chunk !== "string") throw new TypeError("guardStream needs getText for non-string chunks");
|
|
2778
|
+
return chunk;
|
|
2779
|
+
});
|
|
2780
|
+
function* run(source) {
|
|
2781
|
+
try {
|
|
2782
|
+
for (const chunk of source) {
|
|
2783
|
+
stream.feedText(extract(chunk));
|
|
2784
|
+
yield chunk;
|
|
2785
|
+
}
|
|
2786
|
+
} finally {
|
|
2787
|
+
stream.close();
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
async function* runAsync(source) {
|
|
2791
|
+
try {
|
|
2792
|
+
for await (const chunk of source) {
|
|
2793
|
+
stream.feedText(extract(chunk));
|
|
2794
|
+
yield chunk;
|
|
2795
|
+
}
|
|
2796
|
+
} finally {
|
|
2797
|
+
stream.close();
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
return Symbol.asyncIterator in Object(chunks) ? runAsync(chunks) : run(chunks);
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2638
2803
|
// src/voice-pricing.ts
|
|
2639
2804
|
var VOICE_MAP = cost_map_default["__voice__"] ?? {};
|
|
2640
2805
|
var unitForMode = {
|
|
@@ -2743,12 +2908,15 @@ function vapi(guard, options = {}) {
|
|
|
2743
2908
|
FloeGuardError,
|
|
2744
2909
|
LatencyBudget,
|
|
2745
2910
|
LedgerSyncError,
|
|
2911
|
+
StreamGuard,
|
|
2746
2912
|
TokenBudgetExceeded,
|
|
2747
2913
|
UnpriceableModelError,
|
|
2748
2914
|
UnpriceableVoiceError,
|
|
2915
|
+
approxTokens,
|
|
2749
2916
|
budgetGuardMiddleware,
|
|
2750
2917
|
costMapGeneratedAt,
|
|
2751
2918
|
gates,
|
|
2919
|
+
guardStream,
|
|
2752
2920
|
lookupVoiceRate,
|
|
2753
2921
|
priceTokens,
|
|
2754
2922
|
priceVoiceLeg,
|