floe-guard 0.15.7 → 0.17.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 CHANGED
@@ -12,9 +12,14 @@ your process.
12
12
  Works with both **AI SDK v4 and v5** (`ai@4` / `ai@5`).
13
13
 
14
14
  ```bash
15
- npm i floe-guard ai @ai-sdk/openai
15
+ npm i floe-guard ai@5 @ai-sdk/openai@2
16
16
  ```
17
17
 
18
+ This example uses AI SDK 5 with its compatible OpenAI provider, version 2.
19
+ For AI SDK 4, use `ai@4 @ai-sdk/openai@1`. Keep the SDK and provider major
20
+ versions compatible; installing the latest provider can select a model API
21
+ that this middleware does not support.
22
+
18
23
  ```ts
19
24
  import { wrapLanguageModel } from "ai";
20
25
  import { openai } from "@ai-sdk/openai";
@@ -34,6 +39,79 @@ The middleware sits in the call path: it `check()`s before `doGenerate` /
34
39
  `doStream` (throwing `BudgetExceeded` to halt the run) and `record()`s priced
35
40
  token usage after — for streaming it reads usage from the `finish` part.
36
41
 
42
+ ## Mid-stream budget enforcement
43
+
44
+ `StreamGuard` and `guardStream` port Python's streaming USD guard. They price
45
+ each text delta before it reaches the consumer. When a chunk crosses the
46
+ shared ceiling, its partial spend is recorded in `guard.spendLog` **before**
47
+ `BudgetExceeded` is thrown. Active streams on the same guard share the ceiling.
48
+
49
+ ```ts
50
+ import { BudgetGuard, guardStream } from "floe-guard";
51
+
52
+ const guard = new BudgetGuard(0.01);
53
+ // textDeltas is your provider's Iterable<string> or AsyncIterable<string>.
54
+ for await (const text of guardStream(guard, "gpt-4o", textDeltas)) {
55
+ consume(text);
56
+ }
57
+ ```
58
+
59
+ For structured chunks, supply `{ getText: chunk => chunk.delta.text ?? "" }`
60
+ using your provider's actual shape. Without an extractor, non-string chunks
61
+ throw instead of silently recording zero. Synchronous inputs return a
62
+ synchronous iterator; asynchronous inputs return an asynchronous iterator.
63
+
64
+ Use `StreamGuard` directly when the provider reports final usage:
65
+
66
+ ```ts
67
+ import { StreamGuard } from "floe-guard";
68
+
69
+ const reserved = guard.reserve(guard.estimateCall("gpt-4o", 100, 200));
70
+ const stream = new StreamGuard(guard, "gpt-4o", { promptTokens: 100, reserved });
71
+ try {
72
+ for await (const text of textDeltas) {
73
+ stream.feedText(text); // or feedTokens(n) with known per-chunk counts
74
+ consume(text);
75
+ }
76
+ stream.finish({ promptTokens: reportedPromptTokens, completionTokens: reportedCompletionTokens });
77
+ } finally {
78
+ stream.close(); // idempotent; settles estimates if finish() was not reached
79
+ }
80
+ ```
81
+
82
+ - Options: `promptTokens`, `reserved`, `price`, `label`, and `countTokens(delta)`.
83
+ `guardStream` additionally accepts `getText(chunk)`.
84
+ Configuration is captured at construction; changing the caller's options or
85
+ manual price object later does not change an existing stream.
86
+ - `approxTokens` defaults to roughly four Unicode characters per token, with
87
+ a minimum of one for a non-empty delta. A custom tokenizer can replace it.
88
+ `finish()` reconciles to reported usage; `completionTokens` exposes the running
89
+ estimate. Mid-stream checks cover the aggregate USD ceiling, matching Python;
90
+ token reservations are reconciled at settlement.
91
+ - An unpriceable model fails at construction and releases its reservation.
92
+ With `failClosed: false`, unpriceable streams pass through and settlement
93
+ warns and skips accounting, matching the existing guard policy.
94
+ - The wrapper settles on exhaustion, source/consumer errors, and early `break`,
95
+ and closes the source iterator when iteration ends early. Direct users must
96
+ call `close()` in `finally`. If a wrapper is **never iterated**, its reservation
97
+ remains the caller's responsibility: call `guard.release(reserved)`.
98
+ - The crossing chunk has already been generated. With accurate counts, the
99
+ local cutoff can overshoot by that chunk; heuristic error, parallel streams,
100
+ provider buffering and delayed cancellation can cause additional actual
101
+ spend. Stopping iteration requests iterator cleanup, not guaranteed remote
102
+ cancellation. Connect cleanup to your provider's abort mechanism where needed.
103
+ - Existing Vapi, LiveKit and middleware streaming behavior is unchanged; this
104
+ is the standalone primitive requested in issue #124.
105
+
106
+ Run the no-key example from a repository checkout:
107
+
108
+ ```bash
109
+ cd js
110
+ npm ci
111
+ npm run build
112
+ node ../examples/streaming_guard.mjs
113
+ ```
114
+
37
115
  ## Pricing
38
116
 
39
117
  Tokens are priced **offline** from a bundled
@@ -44,7 +44,7 @@ var BudgetExceeded = class extends FloeGuardError {
44
44
  this.limitUsd = limitUsd;
45
45
  }
46
46
  };
47
- var UnpriceableVoiceError = class extends FloeGuardError {
47
+ var UnpriceableLegError = class extends FloeGuardError {
48
48
  vendor;
49
49
  mode;
50
50
  constructor(vendor, mode) {
@@ -1399,7 +1399,7 @@ function resolveVoiceRate(model, mode, override) {
1399
1399
  return { mode, unit: unitForMode[mode], rate: override, source: "override" };
1400
1400
  }
1401
1401
  const rate = lookupVoiceRate(model, mode);
1402
- if (rate === null) throw new UnpriceableVoiceError(model ?? null, mode);
1402
+ if (rate === null) throw new UnpriceableLegError(model ?? null, mode);
1403
1403
  return { mode, unit: unitForMode[mode], rate, source: "cost_map" };
1404
1404
  }
1405
1405
  function voiceLegCost(mode, quantity, rate) {