@bivelio/savings-layer 0.4.21 → 0.4.24

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
@@ -19,8 +19,9 @@
19
19
  Every request your app sends to an AI is paid for by the text going in and out.
20
20
  **Savings** sits on that path and lets through only what's needed — skipping calls
21
21
  that don't have to happen, and trimming what does — so you pay for less. It is not a
22
- promise you take on faith: **every saving is measured with the provider's official
23
- meter, the same one on your bill**, and handed back to you request by request.
22
+ promise you take on faith: **every saving we charge on is measured with the provider's
23
+ official meter, the same one on your bill**, and every saving — measured or estimated,
24
+ and labelled as such — is handed back to you request by request.
24
25
 
25
26
  **We only earn when you save.** Your first month carries no commission at all: we
26
27
  measure everything and show you the statement of what it would have cost. After
@@ -40,8 +41,10 @@ goes straight from your app to your provider; the SDK is never a middleman serve
40
41
 
41
42
  > **Commercial software — © 2026 BiVelio Inc.** Free to install, **paid to run**:
42
43
  > BV‑SALA is fail‑closed and executes only with an active BiVelio license key.
43
- > License checks are **offline** and **your prompts never leave your infrastructure** —
44
- > only counts and cost are ever reported. Use is governed by the [LICENSE](./LICENSE)
44
+ > The license signature is verified **offline**; with `licenseServerUrl` set, the SDK
45
+ > also calls `savings.bivelio.com` for the revocation list, license renewal and usage
46
+ > metering. **Your prompts never leave your infrastructure** — what travels is token
47
+ > counts, cost, the model id and opaque seat/session ids, never content. Use is governed by the [LICENSE](./LICENSE)
45
48
  > and the BiVelio Terms. Not a wrapper around any third‑party library.
46
49
 
47
50
  ## Where it installs
@@ -64,14 +67,55 @@ story, in plain words:
64
67
 
65
68
  **Claude Code, Codex CLI, Goose or Qwen Code with an API key?** The package also
66
69
  ships a local measuring gateway: `bvsala claude` (or `bvsala codex`, `bvsala
67
- goose`, `bvsala qwen`) launches the terminal through it and your real spend
68
- shows up in your dashboard, token by token — nothing to configure, nothing
69
- persisted. For opencode, Crush, Droid, Cline or Kilo Code, `bvsala doctor`
70
- prints the exact config block for each one you have installed. It measures every
71
- request. With a PRO license it also applies one saving by default: it shortens the
72
- cache-write TTL while your session stays hot, which changes the price, never the
73
- content the model reads. The other levers (sibling model, per-turn effort) stay off
74
- until you switch them on.
70
+ goose`, `bvsala qwen`) launches the terminal through it and your real spend shows
71
+ up in your dashboard, token by token — nothing to configure, nothing persisted.
72
+ For opencode, Crush, Droid, Cline or Kilo Code, `bvsala doctor` prints the exact
73
+ config block for each one you have installed. Like the SDK, the gateway is
74
+ fail‑closed: it starts only with your service key (`BIVELIO_SERVICE_KEY`) and an
75
+ active BiVelio license, which it collects with that key and verifies against the
76
+ key embedded in the package. Once running it never interrupts a request in flight:
77
+ if the license lapses, only new requests are refused (HTTP 402) until it is
78
+ renewed. It measures every request. With a PRO license it also applies one saving
79
+ by default: it shortens the cache-write TTL while your session stays hot, which
80
+ changes the price, never the content the model reads. The other levers (sibling
81
+ model, per-turn effort) stay off until you switch them on. On a Claude or ChatGPT
82
+ subscription there is no per-token invoice, so that traffic is metered at
83
+ list-price equivalent and never carries commission; the per-seat license applies
84
+ as for any other use.
85
+
86
+ ### Python and any other language: the local gateway
87
+
88
+ The same gateway sits in front of any SDK that lets you set a base URL, whatever
89
+ the language:
90
+
91
+ ```bash
92
+ bvsala gateway --upstream openai --port 8403 # one gateway per upstream
93
+ bvsala gateway # Anthropic, the default, on 8402
94
+ ```
95
+
96
+ ```python
97
+ from openai import OpenAI
98
+ client = OpenAI(base_url="http://127.0.0.1:8403/v1") # WITH /v1
99
+ stream = client.chat.completions.create(
100
+ model="gpt-4o-mini", messages=[{"role": "user", "content": "Hi"}], stream=True,
101
+ stream_options={"include_usage": True}, # without it a stream carries no usage and is not metered
102
+ )
103
+ for chunk in stream:
104
+ if chunk.choices: # the last chunk carries only the usage
105
+ print(chunk.choices[0].delta.content or "", end="")
106
+
107
+ from anthropic import Anthropic
108
+ claude = Anthropic(base_url="http://127.0.0.1:8402") # NO /v1
109
+ ```
110
+
111
+ With `BIVELIO_SERVICE_KEY` set, the gateway measures every request whose usage the
112
+ provider reports — Anthropic Messages, OpenAI Chat Completions and Responses, streamed
113
+ or not — for models with a list price in its catalog, priced at that list price. A
114
+ request it cannot meter (a model without a list price, a body it cannot read, a stream
115
+ that never asked for usage) is still forwarded, and its log line says so and why. That
116
+ is measurement, not savings: the saving levers of the SDK are TypeScript, and the
117
+ gateway's own levers apply to Anthropic `/v1/messages` traffic with a PRO license.
118
+ `bvsala doctor` prints the recipe for each SDK.
75
119
 
76
120
  ## Check it before you install
77
121
 
@@ -87,25 +131,34 @@ lives in your dashboard once you sign in.
87
131
  **1 · The package**
88
132
 
89
133
  ```bash
90
- pnpm add @bivelio/savings-layer
91
- # or: npm i @bivelio/savings-layer · yarn add @bivelio/savings-layer
134
+ pnpm add @bivelio/savings-layer gpt-tokenizer
135
+ # or: npm i @bivelio/savings-layer gpt-tokenizer · yarn add @bivelio/savings-layer gpt-tokenizer
92
136
  ```
93
137
 
138
+ `gpt-tokenizer` is optional, but install it if you call OpenAI models: it is the
139
+ provider's own tokenizer, and the SDK loads it by itself. Without it, counts fall back
140
+ to a built‑in estimate and the reconstructed savings are less accurate. Either way, a
141
+ saving where the layer removed something is reported as `estimated`, and estimated
142
+ savings are never billed — the SDK warns once when the tokenizer is missing.
143
+
94
144
  **2 · The license** — from your [dashboard](https://savings.bivelio.com/dashboard/install)
95
145
  (**€4 / seat / month** or **€39 / seat / year**, EUR)
96
146
 
97
147
  ```bash
98
148
  export BIVELIO_LICENSE_KEY=… # your license key
99
- export BIVELIO_SERVICE_KEY=… # service-account key: auto-renewal + seat metering
149
+ export BIVELIO_SERVICE_KEY=… # service-account key: auto-renewal + seat metering (required by the gateway)
100
150
  ```
101
151
 
102
- **3 · Wrap the call** — this is the entire code change:
152
+ **3 · Wrap the call** — this is the entire code change. The package is ESM only:
153
+ put this in a `"type": "module"` project or a `.mts` file.
103
154
 
104
155
  ```ts
105
156
  import { createSavingsLayer, OpenAICompatibleProvider } from "@bivelio/savings-layer";
106
157
 
107
158
  // Your provider. LLM traffic goes straight to it — the SDK never proxies it.
108
159
  // Any OpenAI-compatible endpoint works: OpenAI, Azure, vLLM, SGLang, a gateway.
160
+ // Model ids are qualified (`openai/gpt-4o-mini`); the prefix is stripped on the
161
+ // wire for OpenAI and Azure and kept for gateways — see `stripProviderPrefix`.
109
162
  const provider = new OpenAICompatibleProvider({
110
163
  baseUrl: "https://api.openai.com/v1",
111
164
  apiKey: process.env.OPENAI_API_KEY,
@@ -114,7 +167,7 @@ const provider = new OpenAICompatibleProvider({
114
167
  const layer = createSavingsLayer({
115
168
  provider,
116
169
  licenseKey: process.env.BIVELIO_LICENSE_KEY, // required — from your savings.bivelio.com dashboard
117
- licenseServerUrl: "https://savings.bivelio.com", // license check + kill-switch
170
+ licenseServerUrl: "https://savings.bivelio.com", // revocation list, renewal + metering — omit it and the two lines below do nothing
118
171
  serviceAccountKey: process.env.BIVELIO_SERVICE_KEY, // seat metering + AUTO-RENEWAL pickup
119
172
  });
120
173
 
@@ -124,12 +177,56 @@ const { text, report } = await layer.generate({
124
177
  });
125
178
 
126
179
  console.log(text); // "8" (computed locally — no LLM call)
127
- console.log(report.calls.avoidedLlm); // → whole calls you never paid for
128
- console.log(report.cost.netSavings); // → verified savings, per request
180
+ console.log(report.calls.avoidedLlm); // → 1: a whole call you never paid for
181
+ console.log(report.cost.netSavings); // → 0..1 ratio of the baseline you did not pay (1 here)
182
+ console.log(report.cost.baselineEstimate - report.cost.actual); // → the same saving in USD
183
+ console.log(report.cost.basis); // → "estimated": nothing ran, so there is no provider counter
129
184
  ```
130
185
 
131
- That's it. From the first request, `report` carries the saving — measured, not
132
- estimated.
186
+ That's it. From the first request, `report` carries the saving and says how it
187
+ was obtained: `actual` is what the provider metered; `basis` is `"measured"` when
188
+ the request went through untouched, `"reference"` when a repeat of a byte‑identical
189
+ request was served without calling the provider — its baseline is the provider's own
190
+ counter for the identical request that already ran and that you already paid
191
+ (`report.cache.reference` points to it; with an output schema, only when your schema
192
+ traveled exactly as sent, in strict mode, in the provider's native response‑format
193
+ parameter, with nothing added to the prompt), valued at the provider's cached‑input
194
+ rate, the lowest it bills for repeating a prompt; the output counts only under deterministic
195
+ sampling (`temperature: 0`), on a model with a list price in the catalog that does not
196
+ reason and honors that setting, and when the call that ran carried no hidden reasoning (the
197
+ provider reported zero reasoning tokens or, where it does not break them out, its answer
198
+ had no reasoning block), and then at 95 % of its counter, a margin for the small drift
199
+ `temperature: 0` still has —
200
+ `"estimated"` when the layer removed something (the baseline is then reconstructed
201
+ with the model's tokenizer, or with a calibrated estimate where no exact tokenizer
202
+ exists, as for Anthropic models), and `"unmeasured"` when the provider returned no
203
+ usage. Only `measured` and `reference` figures reach your invoice; estimated ones
204
+ never do.
205
+
206
+ **Streaming.** `generateStream()` takes the same request and hands you the answer as
207
+ the provider writes it, then the same `SavingsReport`:
208
+
209
+ ```ts
210
+ const stream = layer.generateStream({
211
+ model: "openai/gpt-4o-mini",
212
+ messages: [{ role: "user", content: "Summarize the release notes in one line." }],
213
+ });
214
+ for await (const event of stream) {
215
+ if (event.type === "text") process.stdout.write(event.delta);
216
+ }
217
+ const { report: streamed } = await stream.result;
218
+ console.log(streamed.cost.basis); // same rules as generate(), read from the provider's final usage
219
+ console.log(streamed.streaming?.delivery); // "live", or "whole" when the answer arrived in one piece
220
+ ```
221
+
222
+ The SDK asks the provider for its final usage at the end of every stream (on
223
+ OpenAI‑compatible endpoints, `stream_options.include_usage`); a stream that ends
224
+ without it is reported as `unmeasured` and never billed. With an output contract, or
225
+ a request declared `deferrable`, the answer is produced exactly as `generate()` would
226
+ and arrives as a single `text` event. Breaking out of the loop while the provider is
227
+ still writing cancels its request, and nothing is cached or recorded; once its answer
228
+ is complete, or when it arrives in one piece, nothing is cancelled.
229
+ `await stream.result` tells you which happened.
133
230
 
134
231
  ## What you get on every request
135
232
 
@@ -148,6 +245,56 @@ are chosen by counting real tokens with the destination model's own tokenizer, a
148
245
  BV‑SALA only moves away from plain JSON when the saving clearly clears the bar —
149
246
  `report.serialization` shows what was chosen and what each candidate would have cost.
150
247
 
248
+ <details>
249
+ <summary><b>Your own A/B — verification sampling (opt-in, off by default)</b></summary>
250
+
251
+ The per-request figures above are what the layer can show without running your
252
+ request twice. To see what it does on your own traffic, with both sides metered by
253
+ your provider, turn on verification sampling:
254
+
255
+ ```ts
256
+ const checkedLayer = createSavingsLayer({
257
+ provider,
258
+ licenseKey: process.env.BIVELIO_LICENSE_KEY,
259
+ licenseServerUrl: "https://savings.bivelio.com",
260
+ serviceAccountKey: process.env.BIVELIO_SERVICE_KEY,
261
+ verification: { sampleRate: 0.02 }, // about 2 in every 100 eligible requests
262
+ });
263
+ ```
264
+
265
+ For that share of eligible requests, after your answer has been returned, the SDK
266
+ sends the same request once more to your provider exactly as it would go without the
267
+ layer — the model you asked for, all your tools, your data inline, no cache marks —
268
+ and discards the second answer. Each pair (what the unoptimized copy cost and what the
269
+ optimized request cost, both from your provider's usage counters) appears under
270
+ **Proof › Your bench** in your dashboard, with the mean difference, its 95 % interval
271
+ and every pair as a CSV download.
272
+
273
+ - **It costs you money.** You pay your provider for every copy: roughly the sampling
274
+ rate times what that traffic would cost without the layer. The SDK says so once
275
+ per process when it starts (`silenceWarnings: true` mutes that notice too).
276
+ - **It is never billed.** Pairs travel on their own channel into their own table.
277
+ They never enter measured savings, your statement or any commission.
278
+ - **Only requests served intact are checked.** Requests that took the fail-open path,
279
+ were cut off, missed their output contract or came back without a provider counter
280
+ are counted as excluded and never sent twice. `savings: { verification: false }`
281
+ excludes a single request.
282
+ - **Both sides follow the same rule.** A pair is compared only when the copy also
283
+ finished normally and met your contract; a copy cut off by the output limit is
284
+ counted and exported, never scored as a saving.
285
+ - **The headline is the floor, and it waits.** It is the lower bound of the 95 %
286
+ interval, shown once there are enough pairs overall and groups with too few pairs
287
+ of their own are a small share of your traffic, so a group where the layer costs
288
+ you money cannot be left out of it.
289
+ - **Only counts leave your infrastructure.** The copy goes to your own provider, like
290
+ the original; BiVelio receives token counts, costs, model ids and outcomes — never
291
+ prompts or answers.
292
+ - **`close()` settles every copy still running:** it is recorded as abandoned, never
293
+ as a pair. On serverless platforms, pass your runtime's `waitUntil` as
294
+ `verification.waitUntil` so checks can finish.
295
+
296
+ </details>
297
+
151
298
  <details>
152
299
  <summary><b>Structured output — validated <code>data</code>, not just text</b></summary>
153
300
 
@@ -243,8 +390,10 @@ BVSALA_SEAT_ID=worker-03 # stable per replica; [A-Za-z0-9._:/-], max 128 chars
243
390
 
244
391
  - **Node ≥ 20.** The package is **ESM‑only** (`"type": "module"`); from CommonJS,
245
392
  `require()` works on Node ≥ 22.
246
- - Optional peer `gpt-tokenizer` for exact OpenAI token counts — without it a
247
- built‑in estimator is used, so it is not required to get started.
393
+ - Optional peer `gpt-tokenizer` for exact OpenAI token counts, loaded automatically
394
+ when installed — without it a built‑in estimator is used. It improves the accuracy
395
+ of estimated figures; what reaches your invoice is `measured` and `reference`, which
396
+ come from the provider's own counter either way.
248
397
  - Every component (cache, vector store, tokenizer, compressor, constraint backend)
249
398
  ships with an in‑memory default and is independently swappable behind the same
250
399
  interfaces.
@@ -255,7 +404,7 @@ BVSALA_SEAT_ID=worker-03 # stable per replica; [A-Za-z0-9._:/-], max 128 chars
255
404
 
256
405
  BV‑SALA is commercial, per‑seat software (**€4 / seat / month** or **€39 / seat /
257
406
  year**, EUR; VAT not included). No free tier and no trial: what you can check before
258
- paying is the published measurement. The license is a signed token your SDK verifies **offline**; the
407
+ paying is the published measurement. The license is a signed token your SDK and the gateway verify **offline**; the
259
408
  managed control plane and the revocation kill‑switch are served from BiVelio. Your
260
409
  LLM traffic goes **straight to your provider** — BiVelio sees counts and cost,
261
410
  never prompts or completions.