@bivelio/savings-layer 0.4.22 → 0.4.25
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/LICENSE +105 -21
- package/README.md +545 -39
- package/dist/adapters/ai-sdk.d.ts +46 -0
- package/dist/adapters/ai-sdk.js +84 -0
- package/dist/adapters/anthropic-sdk.d.ts +35 -0
- package/dist/adapters/anthropic-sdk.js +99 -0
- package/dist/adapters/langchain.d.ts +29 -0
- package/dist/adapters/langchain.js +57 -0
- package/dist/adapters/mastra.d.ts +41 -0
- package/dist/adapters/mastra.js +74 -0
- package/dist/gateway/bin.js +751 -308
- package/dist/index.d.ts +2609 -1461
- package/dist/index.js +1121 -718
- package/docs/terminales/README.md +193 -14
- package/docs/terminales/claude-code.md +11 -4
- package/docs/terminales/cline.md +1 -1
- package/docs/terminales/codex.md +12 -11
- package/docs/terminales/crush.md +1 -1
- package/docs/terminales/droid.md +1 -1
- package/docs/terminales/goose.md +1 -1
- package/docs/terminales/kilo.md +1 -1
- package/docs/terminales/opencode.md +1 -1
- package/docs/terminales/qwen-code.md +1 -1
- package/package.json +70 -4
package/README.md
CHANGED
|
@@ -19,16 +19,24 @@
|
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
promise you take on faith: **every saving we charge on is measured with your provider's
|
|
23
|
+
own usage counter, the one your bill is built from** (for a repeat served without
|
|
24
|
+
calling the provider, the counter of the identical call that ran), and every saving —
|
|
25
|
+
measured or estimated, and labelled as such — is handed back to you request by request.
|
|
26
|
+
|
|
27
|
+
**A small per‑seat fee, and a commission only on savings measured against your
|
|
28
|
+
provider's counter.** The seat fee is paid whether or not you save. Your
|
|
29
|
+
first month carries no commission at all — it is the first calendar month (UTC) in
|
|
30
|
+
which your organization reports usage, so starting late in a month makes it a short one —
|
|
31
|
+
and at its close you see the statement of what the commission would have been. After
|
|
32
|
+
that, the commission is charged on the **measured** saving only — never from estimates:
|
|
33
|
+
a tiered rate from 10 % down to 4 % of what you saved, so **you keep
|
|
34
|
+
at least 90 % of every euro saved** (before the invoice is rounded to the cent; the
|
|
35
|
+
seat fee is separate). A month with no measured saving is zero commission. And your
|
|
36
|
+
prompts and completions **never reach BiVelio**: what BiVelio receives is per‑request
|
|
37
|
+
usage — token counts, cost, the model id and opaque ids — never your content
|
|
38
|
+
([LICENSE](./LICENSE) §5 lists every field; [savings.bivelio.com/privacy](https://savings.bivelio.com/privacy)
|
|
39
|
+
explains each one and how long it is kept).
|
|
32
40
|
|
|
33
41
|
### For engineers, in one line
|
|
34
42
|
|
|
@@ -42,9 +50,11 @@ goes straight from your app to your provider; the SDK is never a middleman serve
|
|
|
42
50
|
> BV‑SALA is fail‑closed and executes only with an active BiVelio license key.
|
|
43
51
|
> The license signature is verified **offline**; with `licenseServerUrl` set, the SDK
|
|
44
52
|
> also calls `savings.bivelio.com` for the revocation list, license renewal and usage
|
|
45
|
-
> metering. **Your prompts never
|
|
46
|
-
> counts, cost, the model id
|
|
47
|
-
> and the
|
|
53
|
+
> metering. **Your prompts, completions and provider keys never reach BiVelio** — what
|
|
54
|
+
> travels is per‑request usage: token counts, cost, the model id, a request id and
|
|
55
|
+
> opaque seat and session ids (the exact list is in [LICENSE](./LICENSE) §5). Use is
|
|
56
|
+
> governed by the LICENSE, which also sets out the seat fee and the commission, and the
|
|
57
|
+
> BiVelio Terms. Not a wrapper around any third‑party library.
|
|
48
58
|
|
|
49
59
|
## Where it installs
|
|
50
60
|
|
|
@@ -66,14 +76,69 @@ story, in plain words:
|
|
|
66
76
|
|
|
67
77
|
**Claude Code, Codex CLI, Goose or Qwen Code with an API key?** The package also
|
|
68
78
|
ships a local measuring gateway: `bvsala claude` (or `bvsala codex`, `bvsala
|
|
69
|
-
goose`, `bvsala qwen`) launches the terminal through it and your real spend
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
goose`, `bvsala qwen`) launches the terminal through it and your real spend shows
|
|
80
|
+
up in your dashboard, token by token — nothing to configure, nothing persisted.
|
|
81
|
+
For opencode, Crush, Droid, Cline or Kilo Code, `bvsala doctor` prints the exact
|
|
82
|
+
config block for each one you have installed. Like the SDK, the gateway is
|
|
83
|
+
fail‑closed: it starts only with your service key (`BIVELIO_SERVICE_KEY`) and an
|
|
84
|
+
active BiVelio license, which it collects with that key and verifies against the
|
|
85
|
+
key embedded in the package. Once running it never interrupts a request in flight:
|
|
86
|
+
if the license lapses, only new requests are refused (HTTP 402) until it is
|
|
87
|
+
renewed. It measures every request. With a PRO license it also applies one saving
|
|
88
|
+
by default: it shortens the cache-write TTL while your session stays hot, which
|
|
89
|
+
changes the price, never the content the model reads. The other levers (sibling
|
|
90
|
+
model, per-turn effort) stay off until you switch them on. On a Claude or ChatGPT
|
|
91
|
+
subscription there is no per-token invoice, so that traffic is metered at
|
|
92
|
+
list-price equivalent and never carries commission; the per-seat license applies
|
|
93
|
+
as for any other use.
|
|
94
|
+
|
|
95
|
+
**The gateway forwards what your terminal sends.** With no lever armed, your
|
|
96
|
+
provider receives the request body byte for byte and every header your client
|
|
97
|
+
sends (see the limits): `cache_control` and its 1-hour TTL, tool search and
|
|
98
|
+
`tool_reference` blocks, the advisor, every `anthropic-beta` value. An armed lever
|
|
99
|
+
changes only what it declares, and a request it cannot rewrite exactly travels
|
|
100
|
+
untouched. A test suite checks this against a fake upstream on every change
|
|
101
|
+
(`test/gateway-no-empeora.test.ts`; the repository is private, so the linked
|
|
102
|
+
section lists exactly what it checks). The gateway also logs, once per session,
|
|
103
|
+
requests that come out worse than your client asked for: cache markers the
|
|
104
|
+
provider ignored, a 1-hour TTL written at 5 minutes upstream, and tool search off
|
|
105
|
+
(Claude Code turns it off by default behind any proxy, this one included;
|
|
106
|
+
`bvsala claude` turns it back on). What it checks, and its limits:
|
|
107
|
+
[docs/terminales/README.md](./docs/terminales/README.md#what-the-gateway-guarantees-it-forwards-what-your-client-sends).
|
|
108
|
+
|
|
109
|
+
### Python and any other language: the local gateway
|
|
110
|
+
|
|
111
|
+
The same gateway sits in front of any SDK that lets you set a base URL, whatever
|
|
112
|
+
the language:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
bvsala gateway --upstream openai --port 8403 # one gateway per upstream
|
|
116
|
+
bvsala gateway # Anthropic, the default, on 8402
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from openai import OpenAI
|
|
121
|
+
client = OpenAI(base_url="http://127.0.0.1:8403/v1") # WITH /v1
|
|
122
|
+
stream = client.chat.completions.create(
|
|
123
|
+
model="gpt-4o-mini", messages=[{"role": "user", "content": "Hi"}], stream=True,
|
|
124
|
+
stream_options={"include_usage": True}, # without it a stream carries no usage and is not metered
|
|
125
|
+
)
|
|
126
|
+
for chunk in stream:
|
|
127
|
+
if chunk.choices: # the last chunk carries only the usage
|
|
128
|
+
print(chunk.choices[0].delta.content or "", end="")
|
|
129
|
+
|
|
130
|
+
from anthropic import Anthropic
|
|
131
|
+
claude = Anthropic(base_url="http://127.0.0.1:8402") # NO /v1
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
With `BIVELIO_SERVICE_KEY` set, the gateway measures every request whose usage the
|
|
135
|
+
provider reports — Anthropic Messages, OpenAI Chat Completions and Responses, streamed
|
|
136
|
+
or not — for models with a list price in its catalog, priced at that list price. A
|
|
137
|
+
request it cannot meter (a model without a list price, a body it cannot read, a stream
|
|
138
|
+
that never asked for usage) is still forwarded, and its log line says so and why. That
|
|
139
|
+
is measurement, not savings: the saving levers of the SDK are TypeScript, and the
|
|
140
|
+
gateway's own levers apply to Anthropic `/v1/messages` traffic with a PRO license.
|
|
141
|
+
`bvsala doctor` prints the recipe for each SDK.
|
|
77
142
|
|
|
78
143
|
## Check it before you install
|
|
79
144
|
|
|
@@ -89,16 +154,22 @@ lives in your dashboard once you sign in.
|
|
|
89
154
|
**1 · The package**
|
|
90
155
|
|
|
91
156
|
```bash
|
|
92
|
-
pnpm add @bivelio/savings-layer
|
|
93
|
-
# or: npm i @bivelio/savings-layer · yarn add @bivelio/savings-layer
|
|
157
|
+
pnpm add @bivelio/savings-layer gpt-tokenizer
|
|
158
|
+
# or: npm i @bivelio/savings-layer gpt-tokenizer · yarn add @bivelio/savings-layer gpt-tokenizer
|
|
94
159
|
```
|
|
95
160
|
|
|
161
|
+
`gpt-tokenizer` is optional, but install it if you call OpenAI models: it is the
|
|
162
|
+
provider's own tokenizer, and the SDK loads it by itself. Without it, counts fall back
|
|
163
|
+
to a built‑in estimate and the reconstructed savings are less accurate. Either way, a
|
|
164
|
+
saving where the layer removed something is reported as `estimated`, and estimated
|
|
165
|
+
savings are never billed — the SDK warns once when the tokenizer is missing.
|
|
166
|
+
|
|
96
167
|
**2 · The license** — from your [dashboard](https://savings.bivelio.com/dashboard/install)
|
|
97
168
|
(**€4 / seat / month** or **€39 / seat / year**, EUR)
|
|
98
169
|
|
|
99
170
|
```bash
|
|
100
171
|
export BIVELIO_LICENSE_KEY=… # your license key
|
|
101
|
-
export BIVELIO_SERVICE_KEY=… # service-account key: auto-renewal + seat metering
|
|
172
|
+
export BIVELIO_SERVICE_KEY=… # service-account key: auto-renewal + seat metering (required by the gateway)
|
|
102
173
|
```
|
|
103
174
|
|
|
104
175
|
**3 · Wrap the call** — this is the entire code change. The package is ESM only:
|
|
@@ -137,11 +208,177 @@ console.log(report.cost.basis); // → "estimated": nothing ran, so there
|
|
|
137
208
|
|
|
138
209
|
That's it. From the first request, `report` carries the saving and says how it
|
|
139
210
|
was obtained: `actual` is what the provider metered; `basis` is `"measured"` when
|
|
140
|
-
the request went through untouched, `"
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
211
|
+
the request went through untouched, `"reference"` when a repeat of a byte‑identical
|
|
212
|
+
request was served without calling the provider — its baseline is the provider's own
|
|
213
|
+
counter for the identical request that already ran and that you already paid
|
|
214
|
+
(`report.cache.reference` points to it; with an output schema, only when your schema
|
|
215
|
+
traveled exactly as sent, in strict mode, in the provider's native response‑format
|
|
216
|
+
parameter, with nothing added to the prompt), valued at the provider's cached‑input
|
|
217
|
+
rate, the lowest it bills for repeating a prompt; the output counts only under deterministic
|
|
218
|
+
sampling (`temperature: 0`), on a model with a list price in the catalog that does not
|
|
219
|
+
reason and honors that setting, and when the call that ran carried no hidden reasoning (the
|
|
220
|
+
provider reported zero reasoning tokens or, where it does not break them out, its answer
|
|
221
|
+
had no reasoning block), and then at 95 % of its counter, a margin for the small drift
|
|
222
|
+
`temperature: 0` still has —
|
|
223
|
+
`"estimated"` when the layer removed something (the baseline is then reconstructed
|
|
224
|
+
with the model's tokenizer, or with a calibrated estimate where no exact tokenizer
|
|
225
|
+
exists, as for Anthropic models), and `"unmeasured"` when the provider returned no
|
|
226
|
+
usage. Only `measured` and `reference` figures reach your invoice; estimated ones
|
|
227
|
+
never do.
|
|
228
|
+
|
|
229
|
+
**Streaming.** `generateStream()` takes the same request and hands you the answer as
|
|
230
|
+
the provider writes it, then the same `SavingsReport`:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
const stream = layer.generateStream({
|
|
234
|
+
model: "openai/gpt-4o-mini",
|
|
235
|
+
messages: [{ role: "user", content: "Summarize the release notes in one line." }],
|
|
236
|
+
});
|
|
237
|
+
for await (const event of stream) {
|
|
238
|
+
if (event.type === "text") process.stdout.write(event.delta);
|
|
239
|
+
}
|
|
240
|
+
const { report: streamed } = await stream.result;
|
|
241
|
+
console.log(streamed.cost.basis); // same rules as generate(), read from the provider's final usage
|
|
242
|
+
console.log(streamed.streaming?.delivery); // "live", or "whole" when the answer arrived in one piece
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The SDK asks the provider for its final usage at the end of every stream (on
|
|
246
|
+
OpenAI‑compatible endpoints, `stream_options.include_usage`); a stream that ends
|
|
247
|
+
without it is reported as `unmeasured` and never billed. With an output contract, or
|
|
248
|
+
a request declared `deferrable`, the answer is produced exactly as `generate()` would
|
|
249
|
+
and arrives as a single `text` event. Breaking out of the loop while the provider is
|
|
250
|
+
still writing cancels its request, and nothing is cached or recorded; once its answer
|
|
251
|
+
is complete, or when it arrives in one piece, nothing is cancelled.
|
|
252
|
+
`await stream.result` tells you which happened.
|
|
253
|
+
|
|
254
|
+
## Inside your framework
|
|
255
|
+
|
|
256
|
+
Already on an agent framework? The layer plugs in at that framework's own extension
|
|
257
|
+
point, from a subpath of this same package. The four adapters behave alike:
|
|
258
|
+
|
|
259
|
+
- **They answer without calling the model when they can** — a computed answer (the
|
|
260
|
+
arithmetic above), an exact repeat of a request that already ran, an identical
|
|
261
|
+
request already in flight — and otherwise let your framework make exactly the call
|
|
262
|
+
it was going to make. They never rewrite what your framework sends.
|
|
263
|
+
- **Tools are treated as having side effects** unless you list them in
|
|
264
|
+
`readOnlyTools`: a request that offers any other tool is never answered from a
|
|
265
|
+
cache, because the action has to run.
|
|
266
|
+
- **Fail‑closed, like the SDK**: without an active license the call fails with
|
|
267
|
+
`LicenseError` (a framework may wrap it, keeping its name and message) and the
|
|
268
|
+
model is not called. **Put the adapter last**, so nothing sits between it and the
|
|
269
|
+
model.
|
|
270
|
+
|
|
271
|
+
Every model call gets the same `SavingsReport` as with the SDK — its `basis` says
|
|
272
|
+
whether a figure comes from your provider's own counter — and `onReport` receives it
|
|
273
|
+
too.
|
|
274
|
+
|
|
275
|
+
### Vercel AI SDK
|
|
276
|
+
|
|
277
|
+
`pnpm add @bivelio/savings-layer ai @ai-sdk/openai` — a `LanguageModelMiddleware`
|
|
278
|
+
for `ai` 6 or 7:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
import { generateText, wrapLanguageModel } from "ai";
|
|
282
|
+
import { openai } from "@ai-sdk/openai";
|
|
283
|
+
import { savingsMiddleware, getSavingsReport } from "@bivelio/savings-layer/ai-sdk";
|
|
284
|
+
|
|
285
|
+
const savings = savingsMiddleware({
|
|
286
|
+
licenseKey: process.env.BIVELIO_LICENSE_KEY,
|
|
287
|
+
licenseServerUrl: "https://savings.bivelio.com",
|
|
288
|
+
serviceAccountKey: process.env.BIVELIO_SERVICE_KEY,
|
|
289
|
+
readOnlyTools: ["get_weather"],
|
|
290
|
+
});
|
|
291
|
+
const model = wrapLanguageModel({ model: openai("gpt-4o-mini"), middleware: savings }); // last in the list
|
|
292
|
+
|
|
293
|
+
const result = await generateText({
|
|
294
|
+
model,
|
|
295
|
+
prompt: "What is 2 + 2 * 3?",
|
|
296
|
+
providerOptions: { bivelio: { context: { sessionId: "user-42" } } },
|
|
297
|
+
});
|
|
298
|
+
console.log(getSavingsReport(result)?.cost.basis); // "estimated": answered without calling the model
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
For `streamText`, read it with `getSavingsReport({ providerMetadata: await result.providerMetadata })`.
|
|
302
|
+
|
|
303
|
+
### Mastra
|
|
304
|
+
|
|
305
|
+
`pnpm add @bivelio/savings-layer @mastra/core` — an input processor for
|
|
306
|
+
`@mastra/core` 1.x:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
import { Agent } from "@mastra/core/agent";
|
|
310
|
+
import { savingsProcessor } from "@bivelio/savings-layer/mastra";
|
|
311
|
+
|
|
312
|
+
const bivelio = savingsProcessor({
|
|
313
|
+
licenseKey: process.env.BIVELIO_LICENSE_KEY,
|
|
314
|
+
licenseServerUrl: "https://savings.bivelio.com",
|
|
315
|
+
serviceAccountKey: process.env.BIVELIO_SERVICE_KEY,
|
|
316
|
+
onReport: (r) => console.log(r.cost.basis),
|
|
317
|
+
});
|
|
318
|
+
const agent = new Agent({
|
|
319
|
+
id: "support",
|
|
320
|
+
name: "Support",
|
|
321
|
+
instructions: "Answer briefly.",
|
|
322
|
+
model: "openai/gpt-4o-mini",
|
|
323
|
+
inputProcessors: [bivelio], // last in the list
|
|
324
|
+
});
|
|
325
|
+
console.log((await agent.generate("What is 2 + 2 * 3?")).text); // "8"
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
It covers `generate()` and `stream()`, not `generateLegacy()`/`streamLegacy()` or a
|
|
329
|
+
durable agent that resumes in another process; identical requests in flight are not
|
|
330
|
+
shared here. `emitReportPart: true` also streams each report as a
|
|
331
|
+
`data-bivelio-savings-report` part.
|
|
332
|
+
|
|
333
|
+
### LangChain.js
|
|
334
|
+
|
|
335
|
+
`pnpm add @bivelio/savings-layer langchain @langchain/core` — an agent middleware for
|
|
336
|
+
`createAgent` in `langchain` 1.x:
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
import { createAgent } from "langchain";
|
|
340
|
+
import { savingsAgentMiddleware, getSavingsReportFromMessage } from "@bivelio/savings-layer/langchain";
|
|
341
|
+
|
|
342
|
+
const agentSavings = savingsAgentMiddleware({
|
|
343
|
+
licenseKey: process.env.BIVELIO_LICENSE_KEY,
|
|
344
|
+
licenseServerUrl: "https://savings.bivelio.com",
|
|
345
|
+
serviceAccountKey: process.env.BIVELIO_SERVICE_KEY,
|
|
346
|
+
});
|
|
347
|
+
const lcAgent = createAgent({ model: "openai:gpt-4o-mini", tools: [], middleware: [agentSavings] }); // last
|
|
348
|
+
const state = await lcAgent.invoke({ messages: [{ role: "user", content: "What is 2 + 2 * 3?" }] });
|
|
349
|
+
console.log(getSavingsReportFromMessage(state.messages.at(-1))?.cost.basis);
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
The model call is measured when it finishes; tokens you stream with
|
|
353
|
+
`streamMode: "messages"` keep coming from the model as usual. A chat model with its
|
|
354
|
+
own `cache` set is reported as `unmeasured`: its counter may be a cached copy.
|
|
355
|
+
|
|
356
|
+
### Anthropic TypeScript SDK
|
|
357
|
+
|
|
358
|
+
`pnpm add @bivelio/savings-layer @anthropic-ai/sdk` — a client middleware for
|
|
359
|
+
`@anthropic-ai/sdk` 0.103 or later:
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
363
|
+
import { anthropicSavings } from "@bivelio/savings-layer/anthropic-sdk";
|
|
364
|
+
|
|
365
|
+
const claudeSavings = anthropicSavings({
|
|
366
|
+
licenseKey: process.env.BIVELIO_LICENSE_KEY,
|
|
367
|
+
licenseServerUrl: "https://savings.bivelio.com",
|
|
368
|
+
serviceAccountKey: process.env.BIVELIO_SERVICE_KEY,
|
|
369
|
+
});
|
|
370
|
+
const client = new Anthropic({ middleware: [claudeSavings.middleware] }); // last in the list
|
|
371
|
+
const message = await client.messages.create({
|
|
372
|
+
model: "claude-opus-5",
|
|
373
|
+
max_tokens: 256,
|
|
374
|
+
messages: [{ role: "user", content: "What is 2 + 2 * 3?" }],
|
|
375
|
+
});
|
|
376
|
+
console.log(claudeSavings.reportFor(message)?.cost.basis);
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
It runs once per HTTP attempt, inside the SDK's retries: an attempt the provider
|
|
380
|
+
rejects reaches the SDK unchanged and is not recorded. Only message creation goes
|
|
381
|
+
through the layer; `countTokens`, batches, models and files pass untouched.
|
|
145
382
|
|
|
146
383
|
## What you get on every request
|
|
147
384
|
|
|
@@ -155,11 +392,130 @@ ledgers** that are never summed into one flattering figure:
|
|
|
155
392
|
| `compressed_tokens` | information kept, but encoded with fewer tokens |
|
|
156
393
|
| `provider_cached_tokens` | tokens still in the prompt, billed/processed more cheaply |
|
|
157
394
|
|
|
395
|
+
**Quality, kept apart from money.** `report.qualityLedger` records how the answer you
|
|
396
|
+
received earned its place: whether it came from your provider, a replay, or the answer
|
|
397
|
+
to a reworded question (with its similarity score); whether it was checked against your
|
|
398
|
+
output contract, repaired or cut off by the output limit; and what the model cascade
|
|
399
|
+
decided. It holds no money, is never added to the four ledgers, and never leaves your
|
|
400
|
+
process. Reusing answers to reworded questions is opt-in, and by default it covers
|
|
401
|
+
single-shot requests only: requests that declare tools, carry tool calls or results, or
|
|
402
|
+
hold more than one user turn are left out unless you set
|
|
403
|
+
`savings.semanticCache: "include-agentic"`.
|
|
404
|
+
|
|
158
405
|
And a promise you can hold it to: **never worse than your JSON.** Payload encodings
|
|
159
406
|
are chosen by counting real tokens with the destination model's own tokenizer, and
|
|
160
407
|
BV‑SALA only moves away from plain JSON when the saving clearly clears the bar —
|
|
161
408
|
`report.serialization` shows what was chosen and what each candidate would have cost.
|
|
162
409
|
|
|
410
|
+
<details>
|
|
411
|
+
<summary><b>Your own A/B — verification sampling (opt-in, off by default)</b></summary>
|
|
412
|
+
|
|
413
|
+
The per-request figures above are what the layer can show without running your
|
|
414
|
+
request twice. To see what it does on your own traffic, with both sides metered by
|
|
415
|
+
your provider, turn on verification sampling:
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
const checkedLayer = createSavingsLayer({
|
|
419
|
+
provider,
|
|
420
|
+
licenseKey: process.env.BIVELIO_LICENSE_KEY,
|
|
421
|
+
licenseServerUrl: "https://savings.bivelio.com",
|
|
422
|
+
serviceAccountKey: process.env.BIVELIO_SERVICE_KEY,
|
|
423
|
+
verification: { sampleRate: 0.02 }, // about 2 in every 100 eligible requests
|
|
424
|
+
});
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
For that share of eligible requests, after your answer has been returned, the SDK
|
|
428
|
+
sends the same request once more to your provider exactly as it would go without the
|
|
429
|
+
layer — the model you asked for, all your tools, your data inline, no cache marks —
|
|
430
|
+
and discards the second answer. Each pair (what the unoptimized copy cost and what the
|
|
431
|
+
optimized request cost, both from your provider's usage counters) appears under
|
|
432
|
+
**Proof › Your bench** in your dashboard, with the mean difference, its 95 % interval
|
|
433
|
+
and every pair as a CSV download.
|
|
434
|
+
|
|
435
|
+
- **It costs you money.** You pay your provider for every copy: roughly the sampling
|
|
436
|
+
rate times what that traffic would cost without the layer. The SDK says so once
|
|
437
|
+
per process when it starts (`silenceWarnings: true` mutes that notice too).
|
|
438
|
+
- **It is never billed.** Pairs travel on their own channel into their own table.
|
|
439
|
+
They never enter measured savings, your statement or any commission.
|
|
440
|
+
- **Only requests served intact are checked.** Requests that took the fail-open path,
|
|
441
|
+
were cut off, missed their output contract or came back without a provider counter
|
|
442
|
+
are counted as excluded and never sent twice. `savings: { verification: false }`
|
|
443
|
+
excludes a single request.
|
|
444
|
+
- **Both sides follow the same rule.** A pair is compared only when the copy also
|
|
445
|
+
finished normally and met your contract; a copy cut off by the output limit is
|
|
446
|
+
counted and exported, never scored as a saving.
|
|
447
|
+
- **The headline is the floor, and it waits.** It is the lower bound of the 95 %
|
|
448
|
+
interval, shown once there are enough pairs overall and groups with too few pairs
|
|
449
|
+
of their own are a small share of your traffic, so a group where the layer costs
|
|
450
|
+
you money cannot be left out of it.
|
|
451
|
+
- **Only counts leave your infrastructure.** The copy goes to your own provider, like
|
|
452
|
+
the original; BiVelio receives token counts, costs, model ids and outcomes — never
|
|
453
|
+
prompts or answers.
|
|
454
|
+
- **`close()` settles every copy still running:** it is recorded as abandoned, never
|
|
455
|
+
as a pair. On serverless platforms, pass your runtime's `waitUntil` as
|
|
456
|
+
`verification.waitUntil` so checks can finish.
|
|
457
|
+
|
|
458
|
+
</details>
|
|
459
|
+
|
|
460
|
+
<details>
|
|
461
|
+
<summary><b>Provider price levers (opt-in, off by default)</b></summary>
|
|
462
|
+
|
|
463
|
+
Two request flags ask your provider for a lower price on the same tokens. Neither
|
|
464
|
+
changes the answer, and what they are worth is reported next to the four ledgers:
|
|
465
|
+
never folded into `cost.*`, never part of any commission.
|
|
466
|
+
|
|
467
|
+
**`savings.deferrable`** asks for the provider's Flex tier: OpenAI, and Gemini through
|
|
468
|
+
its OpenAI‑compatible endpoint. Anthropic has no cheaper synchronous tier (its
|
|
469
|
+
discount lives only in Message Batches, which the layer does not support yet).
|
|
470
|
+
Bedrock and DeepSeek off‑peak pricing are not supported.
|
|
471
|
+
|
|
472
|
+
- **Latency.** OpenAI publishes no latency commitment for Flex; Google publishes a
|
|
473
|
+
target of minutes. Keep deferrable requests off user‑facing paths.
|
|
474
|
+
`report.deferred.latency` quotes what the provider documents.
|
|
475
|
+
- **Risk.** Without capacity the provider refuses the request (429, or 503 on Gemini)
|
|
476
|
+
instead of waiting; OpenAI documents that the refusal is not charged. With
|
|
477
|
+
`priceLevers: { onCapacityError: "standard" }` the layer repeats it once at the
|
|
478
|
+
standard tier and price, and says so in `report.deferred.capacityFallback`.
|
|
479
|
+
- **What it is worth.** Your provider's usage counter for that call, priced at the two
|
|
480
|
+
published rate cards (standard, and the tier that actually served it), component
|
|
481
|
+
by component, with the date the prices were checked. It is `measured` only when
|
|
482
|
+
the served tier was read back from the provider's own API, the prices are list
|
|
483
|
+
prices and the counter says how much of the input was read from (or written to)
|
|
484
|
+
the prompt cache; otherwise `estimated`, and `basisReason` says why. It never
|
|
485
|
+
exceeds the difference between the two published rate cards, even with your own
|
|
486
|
+
`costModels`. A tier the provider did not confirm is never credited.
|
|
487
|
+
|
|
488
|
+
**`savings.standalone`** (GPT‑5.6 and later, on OpenAI's own API) declares that nothing
|
|
489
|
+
after the stable prefix of this request will be sent again. The provider then writes
|
|
490
|
+
its prompt cache only at the end of that prefix, or nowhere, instead of at the end of
|
|
491
|
+
your message, which on these models costs more than plain input. The trade‑off:
|
|
492
|
+
repeats and continuations of this exact prompt lose the cache reads the provider
|
|
493
|
+
default would give them, so leave it off conversation turns you will continue. Its
|
|
494
|
+
figure, `report.cacheWrites.avoidedWritePremium`, is always `estimated`: the provider
|
|
495
|
+
default was not run.
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
const leveredLayer = createSavingsLayer({
|
|
499
|
+
provider,
|
|
500
|
+
licenseKey: process.env.BIVELIO_LICENSE_KEY,
|
|
501
|
+
priceLevers: { deferrable: true, onCapacityError: "standard" }, // layer-wide defaults
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const { report } = await leveredLayer.generate({
|
|
505
|
+
model: "openai/gpt-5.6-luna",
|
|
506
|
+
messages: [
|
|
507
|
+
{ role: "system", content: "…your long, fixed instructions…" },
|
|
508
|
+
{ role: "user", content: "Classify ticket 42." },
|
|
509
|
+
],
|
|
510
|
+
savings: { standalone: true }, // a request's own value always wins
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
report.deferred?.tierSaving; // { amount, basis, standardCost, appliedCost, pricesCheckedOn, basisReason }
|
|
514
|
+
report.cacheWrites; // { mode, breakpoints, avoidedWritePremium?, reason }
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
</details>
|
|
518
|
+
|
|
163
519
|
<details>
|
|
164
520
|
<summary><b>Structured output — validated <code>data</code>, not just text</b></summary>
|
|
165
521
|
|
|
@@ -233,6 +589,144 @@ an action that has not run yet cannot be a saving.
|
|
|
233
589
|
|
|
234
590
|
</details>
|
|
235
591
|
|
|
592
|
+
<details>
|
|
593
|
+
<summary><b>Your ledgers in your tracing — OpenTelemetry, Langfuse, Datadog</b></summary>
|
|
594
|
+
|
|
595
|
+
Wrap the layer once and every request becomes a span on the OpenTelemetry pipeline
|
|
596
|
+
you already run (`@opentelemetry/api` is an optional peer; the SDK never imports it):
|
|
597
|
+
|
|
598
|
+
```ts
|
|
599
|
+
import { trace } from "@opentelemetry/api";
|
|
600
|
+
import { instrumentSavingsLayer } from "@bivelio/savings-layer";
|
|
601
|
+
|
|
602
|
+
// One span per request, on the OpenTelemetry pipeline you already run.
|
|
603
|
+
const traced = instrumentSavingsLayer(layer, { tracer: trace });
|
|
604
|
+
const { report: tracedReport } = await traced.generate({
|
|
605
|
+
model: "openai/gpt-4o-mini",
|
|
606
|
+
messages: [{ role: "user", content: "What is 2 + 2 * 3?" }],
|
|
607
|
+
});
|
|
608
|
+
console.log(tracedReport.cost.basis); // the span carries the same figures
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
- **Standard spans, plus your ledgers.** Spans follow the
|
|
612
|
+
[GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions/blob/v1.41.1/model/gen-ai/registry.yaml)
|
|
613
|
+
as last published (OpenTelemetry semantic conventions v1.41.1): `chat {model}`,
|
|
614
|
+
kind CLIENT, provider, model and usage from your provider's counter (0 on an avoided
|
|
615
|
+
call: no token was billed). On top, `com.bivelio.*` carries the four ledgers, the
|
|
616
|
+
`basis`, the cost and the ids; `com.bivelio.request_id` is the same id as your usage
|
|
617
|
+
record. Same results, same errors: `traced` hands back the very objects `layer` does.
|
|
618
|
+
- **Money is kept apart by basis.** The money saved is
|
|
619
|
+
`com.bivelio.cost.saved.measured`, `.reference` or `.estimated` — one key per span,
|
|
620
|
+
the one of its basis — so a sum over one key never mixes measured and estimated
|
|
621
|
+
money; `unmeasured` spans carry counts and no money. `report.cost.netSavings` is a
|
|
622
|
+
ratio and goes out as `com.bivelio.cost.net_savings_ratio`; the money is
|
|
623
|
+
`baselineEstimate − actual`, and that is what `saved.*` holds.
|
|
624
|
+
- **None of this talks to BiVelio.** Spans go to your exporter; files stay where you
|
|
625
|
+
write them. Your prompts, completions and provider keys never reach BiVelio, and no
|
|
626
|
+
span carries them either: ids, counts, money and enums only, and a failed request
|
|
627
|
+
records its `error.type`, never its message.
|
|
628
|
+
- **Langfuse.** Add the `LangfuseSpanProcessor` from `@langfuse/otel` to your tracer
|
|
629
|
+
provider. These spans carry `gen_ai.*`, so its default filter exports them
|
|
630
|
+
([Langfuse OpenTelemetry docs](https://langfuse.com/integrations/native/opentelemetry)).
|
|
631
|
+
`instrumentSavingsLayer(layer, { tracer: trace, langfuse: { metadata: true } })`
|
|
632
|
+
makes the figures filterable metadata, and `langfuse: { cost: true }` hands Langfuse
|
|
633
|
+
the report's own cost (in USD) instead of its estimate. Tracing with `@langfuse/tracing` by
|
|
634
|
+
hand? Pass `toLangfuseGeneration(report)` to `generation.update()`.
|
|
635
|
+
- **Datadog LLM Observability** reads these spans over OTLP
|
|
636
|
+
([Datadog OpenTelemetry docs](https://docs.datadoghq.com/llm_observability/instrumentation/otel_instrumentation/)):
|
|
637
|
+
|
|
638
|
+
```bash
|
|
639
|
+
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf
|
|
640
|
+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=<your Datadog site's OTLP traces endpoint>
|
|
641
|
+
OTEL_EXPORTER_OTLP_TRACES_HEADERS=dd-api-key=<your Datadog API key>,dd-otlp-source=llmobs
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
The `com.bivelio.*` attributes arrive as tags; with `datadog: { metadata: true }`
|
|
645
|
+
they also arrive as span metadata. Datadog drops spans that carry no `gen_ai.*`
|
|
646
|
+
attribute, so use `instrumentSavingsLayer` there rather than `recordSavingsOnSpan`
|
|
647
|
+
(which writes `com.bivelio.*` on a span you already have, and no `gen_ai.*` unless
|
|
648
|
+
you ask for it with `genAi: true`).
|
|
649
|
+
|
|
650
|
+
| Attribute | What it holds |
|
|
651
|
+
| --- | --- |
|
|
652
|
+
| `gen_ai.operation.name`, `gen_ai.provider.name`, `gen_ai.request.model`, `gen_ai.response.model` | `chat`, the provider and the model (without a known provider prefix) |
|
|
653
|
+
| `gen_ai.usage.input_tokens`, `.output_tokens`, `.cache_read.input_tokens` | your provider's counter; absent when it reported no usage |
|
|
654
|
+
| `com.bivelio.cost.basis`, `.currency`, `.baseline`, `.actual` | where the money comes from, its currency (USD by default), and both sides |
|
|
655
|
+
| `com.bivelio.cost.saved.measured` \| `.reference` \| `.estimated` | the money saved, in the key of its basis only |
|
|
656
|
+
| `com.bivelio.cost.net_savings_ratio` | `cost.netSavings`, a 0..1 ratio — not money |
|
|
657
|
+
| `com.bivelio.calls.*`, `com.bivelio.tokens.*` | the four ledgers: avoided calls, eliminated, compressed and provider‑cached tokens |
|
|
658
|
+
| `com.bivelio.request_id`, `.model.id`, `.cache.result`, `.quality.status` | ids and outcomes |
|
|
659
|
+
|
|
660
|
+
</details>
|
|
661
|
+
|
|
662
|
+
<details>
|
|
663
|
+
<summary><b>Your ledgers in your FinOps tool — FOCUS rows and a daily savings series</b></summary>
|
|
664
|
+
|
|
665
|
+
Keep each report with the time it came back, and export whenever you like:
|
|
666
|
+
|
|
667
|
+
```ts
|
|
668
|
+
import { writeFileSync } from "node:fs";
|
|
669
|
+
import { toFocusRows, toSavingsSeries, toCsv, type SavingsRecord } from "@bivelio/savings-layer";
|
|
670
|
+
|
|
671
|
+
const records: SavingsRecord[] = [];
|
|
672
|
+
const answer = await layer.generate({
|
|
673
|
+
model: "openai/gpt-4o-mini",
|
|
674
|
+
messages: [{ role: "user", content: "What is 2 + 2 * 3?" }],
|
|
675
|
+
});
|
|
676
|
+
records.push({ at: Date.now(), report: answer.report }); // keep reports as they come back
|
|
677
|
+
|
|
678
|
+
// FOCUS 1.4 rows, one per day, model and basis. Every cost column is 0 by design:
|
|
679
|
+
// your provider's bill already has the spend, so these rows add savings, never cost.
|
|
680
|
+
const rows = toFocusRows(records, { billingAccountId: "org-XXXXXXXX" }); // your provider billing account id
|
|
681
|
+
writeFileSync("bv-sala-focus.csv", toCsv(rows));
|
|
682
|
+
|
|
683
|
+
// A daily series for tools that take savings as a metric: one file per basis.
|
|
684
|
+
const measured = toSavingsSeries(records).filter((r) => r.measuredSavings !== null);
|
|
685
|
+
writeFileSync(
|
|
686
|
+
"vantage-measured-savings.csv",
|
|
687
|
+
toCsv(measured.map((r) => ({ date: r.date, amount: r.measuredSavings, model: r.model }))),
|
|
688
|
+
);
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
The rows follow [FOCUS 1.4](https://github.com/FinOps-Open-Cost-and-Usage-Spec/FOCUS_Spec/tree/v1.4),
|
|
692
|
+
the FinOps Open Cost and Usage Specification: `ChargeCategory` is `Adjustment`, the
|
|
693
|
+
service provider is the one in the model id (`OpenAI`, `Anthropic`…; pass
|
|
694
|
+
`serviceProviderName`, `invoiceIssuerName` or `hostProviderName` when a cloud or a
|
|
695
|
+
gateway bills you), and no SKU or unit price is invented. Nothing is sent anywhere:
|
|
696
|
+
`toFocusRows`, `toSavingsSeries` and `toCsv` are plain functions over the reports you
|
|
697
|
+
kept. Where each file goes, as each tool documents it in September 2026:
|
|
698
|
+
|
|
699
|
+
- **A FOCUS data lake or warehouse** reads every column, the `x_BiVelio*` ones included.
|
|
700
|
+
- **[Finout](https://docs.finout.io/get-started-with-finout/cost-and-usage-types/focus-in-finout)**
|
|
701
|
+
loads a FOCUS CSV as a custom cost center; you send the file to Finout support.
|
|
702
|
+
- **[Vantage](https://docs.vantage.sh/connecting_custom_providers)** Custom Providers
|
|
703
|
+
accept FOCUS 1.1–1.3 files but use only a few columns for cost reporting, and say
|
|
704
|
+
nothing about `x_` columns, so the savings would not show. Upload the series as a
|
|
705
|
+
[Business Metric](https://docs.vantage.sh/per_unit_costs) instead
|
|
706
|
+
(`date,amount,<labels>`), one file per basis, so a metric never mixes bases.
|
|
707
|
+
Vantage rejects two rows with the same date and labels: with `dimensions`, add
|
|
708
|
+
them as label columns too.
|
|
709
|
+
- **[CloudZero](https://docs.cloudzero.com/docs/anycost-common-bill-format-cbf)** AnyCost
|
|
710
|
+
takes its own Common Bill Format; the series can go in as a
|
|
711
|
+
[Telemetry Stream](https://docs.cloudzero.com/docs/telemetry-streams).
|
|
712
|
+
|
|
713
|
+
The custom columns, in the currency you pass (`currency`, USD by default):
|
|
714
|
+
|
|
715
|
+
| Column | What it holds |
|
|
716
|
+
| --- | --- |
|
|
717
|
+
| `x_BiVelioBasis`, `x_BiVelioModelId` | the basis of every figure in the row, and the model id |
|
|
718
|
+
| `x_BiVelioRequestCount`, `x_BiVelioProviderCallCount`, `x_BiVelioAvoidedCallCount` | requests, calls that reached the provider, calls avoided |
|
|
719
|
+
| `x_BiVelioBaselineCost`, `x_BiVelioActualCost` | both sides of the saving; empty when the basis is `unmeasured` |
|
|
720
|
+
| `x_BiVelioMeasuredSavings`, `x_BiVelioReferenceSavings`, `x_BiVelioEstimatedSavings` | `baseline − actual`, in the column of the row's basis only; negative when the layer cost you money |
|
|
721
|
+
| `x_BiVelioEliminatedInputFloorCost` | the separate floor lane for context your provider cleared |
|
|
722
|
+
| `x_BiVelio…InputTokens`, `x_BiVelio…OutputTokens` | the token ledgers: baseline, actual, eliminated, compressed, provider‑cached |
|
|
723
|
+
|
|
724
|
+
`Tags` carries `bivelio/basis` and `bivelio/model`, plus any tags you return from the
|
|
725
|
+
`tags` option. Sum `measured` and `reference` for what you can check against your
|
|
726
|
+
provider invoice; `estimated` has its own column on purpose.
|
|
727
|
+
|
|
728
|
+
</details>
|
|
729
|
+
|
|
236
730
|
<details>
|
|
237
731
|
<summary><b>Running a fleet? Seats and renewals</b></summary>
|
|
238
732
|
|
|
@@ -255,8 +749,14 @@ BVSALA_SEAT_ID=worker-03 # stable per replica; [A-Za-z0-9._:/-], max 128 chars
|
|
|
255
749
|
|
|
256
750
|
- **Node ≥ 20.** The package is **ESM‑only** (`"type": "module"`); from CommonJS,
|
|
257
751
|
`require()` works on Node ≥ 22.
|
|
258
|
-
-
|
|
259
|
-
|
|
752
|
+
- Framework adapters: `ai` 6 or 7, `@mastra/core` ≥ 1.68 < 2, `langchain` ≥ 1.1 < 2
|
|
753
|
+
with `@langchain/core` ≥ 1.1 < 2, and `@anthropic-ai/sdk` ≥ 0.103 < 1 — optional
|
|
754
|
+
peers, needed only for the adapter you import. `ai` 7 and `@mastra/core` declare
|
|
755
|
+
Node ≥ 22 in their own `engines`.
|
|
756
|
+
- Optional peer `gpt-tokenizer` for exact OpenAI token counts, loaded automatically
|
|
757
|
+
when installed — without it a built‑in estimator is used. It improves the accuracy
|
|
758
|
+
of estimated figures; what reaches your invoice is `measured` and `reference`, which
|
|
759
|
+
come from the provider's own counter either way.
|
|
260
760
|
- Every component (cache, vector store, tokenizer, compressor, constraint backend)
|
|
261
761
|
ships with an in‑memory default and is independently swappable behind the same
|
|
262
762
|
interfaces.
|
|
@@ -265,14 +765,20 @@ BVSALA_SEAT_ID=worker-03 # stable per replica; [A-Za-z0-9._:/-], max 128 chars
|
|
|
265
765
|
|
|
266
766
|
## Licensing & privacy
|
|
267
767
|
|
|
268
|
-
BV‑SALA is commercial
|
|
269
|
-
year**, EUR; VAT not included)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
768
|
+
BV‑SALA is commercial software: a per‑seat subscription (**€4 / seat / month** or
|
|
769
|
+
**€39 / seat / year**, EUR; VAT not included), paid whether or not you save, plus a
|
|
770
|
+
tiered commission on measured savings only, as described at the top of this page, in
|
|
771
|
+
[LICENSE](./LICENSE) §6 and in the Published Pricing it refers to,
|
|
772
|
+
**[savings.bivelio.com/pricing](https://savings.bivelio.com/pricing)**. No free tier and no trial: what you can check before
|
|
773
|
+
paying is the published measurement. The license is a signed token your SDK and the
|
|
774
|
+
gateway verify **offline**; the managed control plane and the revocation kill‑switch
|
|
775
|
+
are served from BiVelio. Your LLM traffic goes **straight to your provider** —
|
|
776
|
+
BiVelio receives per‑request usage (counts, cost, the model id and opaque ids), never
|
|
777
|
+
prompts, completions or provider keys; [LICENSE](./LICENSE) §5 lists every field, and
|
|
778
|
+
**[savings.bivelio.com/privacy](https://savings.bivelio.com/privacy)** says how long
|
|
779
|
+
each one is kept.
|
|
780
|
+
|
|
781
|
+
Get a key at **[savings.bivelio.com](https://savings.bivelio.com)**.
|
|
276
782
|
Onboarding & general questions: **support@bivelio.com** · licensing & compliance:
|
|
277
783
|
**privacy@bivelio.com** · technical: **team@bivelio.com**.
|
|
278
784
|
|