@bivelio/savings-layer 0.4.24 → 0.4.26
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 +601 -28
- 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 +100 -0
- package/dist/adapters/langchain.d.ts +29 -0
- package/dist/adapters/langchain.js +58 -0
- package/dist/adapters/mastra.d.ts +41 -0
- package/dist/adapters/mastra.js +74 -0
- package/dist/gateway/bin.js +963 -630
- package/dist/index.d.ts +2912 -1516
- package/dist/index.js +1368 -865
- package/docs/terminales/README.md +173 -17
- package/docs/terminales/aider.md +74 -0
- package/docs/terminales/claude-code.md +7 -2
- package/docs/terminales/continue.md +47 -0
- package/docs/terminales/copilot.md +114 -0
- package/docs/terminales/kilo.md +54 -31
- package/docs/terminales/no-soportados.md +41 -0
- package/docs/terminales/opencode.md +33 -11
- package/package.json +89 -5
package/README.md
CHANGED
|
@@ -19,17 +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 we charge on is measured with
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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).
|
|
33
40
|
|
|
34
41
|
### For engineers, in one line
|
|
35
42
|
|
|
@@ -43,9 +50,11 @@ goes straight from your app to your provider; the SDK is never a middleman serve
|
|
|
43
50
|
> BV‑SALA is fail‑closed and executes only with an active BiVelio license key.
|
|
44
51
|
> The license signature is verified **offline**; with `licenseServerUrl` set, the SDK
|
|
45
52
|
> also calls `savings.bivelio.com` for the revocation list, license renewal and usage
|
|
46
|
-
> metering. **Your prompts never
|
|
47
|
-
> counts, cost, the model id
|
|
48
|
-
> 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.
|
|
49
58
|
|
|
50
59
|
## Where it installs
|
|
51
60
|
|
|
@@ -65,12 +74,14 @@ products charge a flat subscription, so there is no per‑token bill to cut. The
|
|
|
65
74
|
story, in plain words:
|
|
66
75
|
**[Where does the saving install?](https://savings.bivelio.com/en/how-it-works)**
|
|
67
76
|
|
|
68
|
-
**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
up in your dashboard, token by token — nothing to configure, nothing
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
**A coding agent with an API key?** The package also ships a local measuring
|
|
78
|
+
gateway: `bvsala claude` (or `bvsala codex`, `goose`, `qwen`, `opencode`, `kilo`,
|
|
79
|
+
`copilot` with your own Anthropic key, `aider`) launches the agent through it and your real
|
|
80
|
+
spend shows up in your dashboard, token by token — nothing to configure, nothing
|
|
81
|
+
persisted. On exit it tells you how many requests it metered and which ones it
|
|
82
|
+
could not, and why. For Crush, Droid, Cline, Continue or Copilot in VS Code,
|
|
83
|
+
`bvsala doctor` prints the exact config block. Gemini CLI and Cursor cannot be
|
|
84
|
+
metered today, and the doctor says so. Like the SDK, the gateway is
|
|
74
85
|
fail‑closed: it starts only with your service key (`BIVELIO_SERVICE_KEY`) and an
|
|
75
86
|
active BiVelio license, which it collects with that key and verifies against the
|
|
76
87
|
key embedded in the package. Once running it never interrupts a request in flight:
|
|
@@ -83,6 +94,21 @@ subscription there is no per-token invoice, so that traffic is metered at
|
|
|
83
94
|
list-price equivalent and never carries commission; the per-seat license applies
|
|
84
95
|
as for any other use.
|
|
85
96
|
|
|
97
|
+
**The gateway forwards what your terminal sends.** With no lever armed, your
|
|
98
|
+
provider receives the request body byte for byte and every header your client
|
|
99
|
+
sends except `x-bvsala-report`, which asks the gateway itself for a report (see
|
|
100
|
+
the limits): `cache_control` and its 1-hour TTL, tool search and
|
|
101
|
+
`tool_reference` blocks, the advisor, every `anthropic-beta` value. An armed lever
|
|
102
|
+
changes only what it declares, and a request it cannot rewrite exactly travels
|
|
103
|
+
untouched. A test suite checks this against a fake upstream on every change
|
|
104
|
+
(`test/gateway-no-empeora.test.ts`; the repository is private, so the linked
|
|
105
|
+
section lists exactly what it checks). The gateway also logs, once per session,
|
|
106
|
+
requests that come out worse than your client asked for: cache markers the
|
|
107
|
+
provider ignored, a 1-hour TTL written at 5 minutes upstream, and tool search off
|
|
108
|
+
(Claude Code turns it off by default behind any proxy, this one included;
|
|
109
|
+
`bvsala claude` turns it back on). What it checks, and its limits:
|
|
110
|
+
[docs/terminales/README.md](./docs/terminales/README.md#what-the-gateway-guarantees-it-forwards-what-your-client-sends).
|
|
111
|
+
|
|
86
112
|
### Python and any other language: the local gateway
|
|
87
113
|
|
|
88
114
|
The same gateway sits in front of any SDK that lets you set a base URL, whatever
|
|
@@ -117,6 +143,17 @@ is measurement, not savings: the saving levers of the SDK are TypeScript, and th
|
|
|
117
143
|
gateway's own levers apply to Anthropic `/v1/messages` traffic with a PRO license.
|
|
118
144
|
`bvsala doctor` prints the recipe for each SDK.
|
|
119
145
|
|
|
146
|
+
Each request can also bring its report back to your code. Send the header
|
|
147
|
+
`x-bvsala-report: 1` (in Python, `default_headers={"x-bvsala-report": "1"}` on
|
|
148
|
+
the client) and the gateway answers with `x-bvsala-request-id`. On the same
|
|
149
|
+
gateway, `GET /_bvsala/v1/reports/<that id>` returns what it recorded for your
|
|
150
|
+
dashboard: `metered` with the `SavingsReport` (the provider's token counts, the
|
|
151
|
+
cost at list price and `cost.basis`), or `not_metered` with the reason from the
|
|
152
|
+
log line. `?response_id=<the provider's response id>` finds it too, and
|
|
153
|
+
`wait_ms` waits for a request still in flight. The header never reaches your
|
|
154
|
+
provider, and a request without it is not changed at all:
|
|
155
|
+
[the details](./docs/terminales/README.md#what-the-gateway-guarantees-it-forwards-what-your-client-sends).
|
|
156
|
+
|
|
120
157
|
## Check it before you install
|
|
121
158
|
|
|
122
159
|
What you can check before paying is what we have measured: the published benchmark,
|
|
@@ -126,6 +163,22 @@ and the commission calculator on the front page, which runs on the same price
|
|
|
126
163
|
catalogue that bills you. The verifier that replays the optimizer on your own data
|
|
127
164
|
lives in your dashboard once you sign in.
|
|
128
165
|
|
|
166
|
+
## Set it up in one command
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
npx @bivelio/savings-layer@latest init
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
It reads your `package.json` (Next.js, Express, the Vercel AI SDK, Mastra,
|
|
173
|
+
LangChain.js, the Anthropic SDK or the OpenAI SDK) and prints the three steps below,
|
|
174
|
+
written for your stack — the install line for your package manager, which keys are
|
|
175
|
+
missing, the exact code for your adapter — plus a free check: a question the layer
|
|
176
|
+
answers on your machine, so the model is not called. It reads `package.json` files only
|
|
177
|
+
(yours, and those in `node_modules` for the installed versions), never a `.env` file or a
|
|
178
|
+
key's value; it uses no network of its own and writes nothing.
|
|
179
|
+
`--write` creates the new files after asking you, and never overwrites one; `--json`
|
|
180
|
+
prints the same plan for a script or a coding agent.
|
|
181
|
+
|
|
129
182
|
## Install in three steps
|
|
130
183
|
|
|
131
184
|
**1 · The package**
|
|
@@ -228,6 +281,137 @@ still writing cancels its request, and nothing is cached or recorded; once its a
|
|
|
228
281
|
is complete, or when it arrives in one piece, nothing is cancelled.
|
|
229
282
|
`await stream.result` tells you which happened.
|
|
230
283
|
|
|
284
|
+
## Inside your framework
|
|
285
|
+
|
|
286
|
+
Already on an agent framework? The layer plugs in at that framework's own extension
|
|
287
|
+
point, from a subpath of this same package. The four adapters behave alike:
|
|
288
|
+
|
|
289
|
+
- **They answer without calling the model when they can** — a computed answer (the
|
|
290
|
+
arithmetic above), an exact repeat of a request that already ran, an identical
|
|
291
|
+
request already in flight — and otherwise let your framework make exactly the call
|
|
292
|
+
it was going to make. They never rewrite what your framework sends.
|
|
293
|
+
- **Tools are treated as having side effects** unless you list them in
|
|
294
|
+
`readOnlyTools`: a request that offers any other tool is never answered from a
|
|
295
|
+
cache, because the action has to run.
|
|
296
|
+
- **Fail‑closed, like the SDK**: without an active license the call fails with
|
|
297
|
+
`LicenseError` (a framework may wrap it, keeping its name and message) and the
|
|
298
|
+
model is not called. **Put the adapter last**, so nothing sits between it and the
|
|
299
|
+
model.
|
|
300
|
+
|
|
301
|
+
Every model call gets the same `SavingsReport` as with the SDK — its `basis` says
|
|
302
|
+
whether a figure comes from your provider's own counter — and `onReport` receives it
|
|
303
|
+
too.
|
|
304
|
+
|
|
305
|
+
### Vercel AI SDK
|
|
306
|
+
|
|
307
|
+
`pnpm add @bivelio/savings-layer ai @ai-sdk/openai` — a `LanguageModelMiddleware`
|
|
308
|
+
for `ai` 6 or 7:
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
import { generateText, wrapLanguageModel } from "ai";
|
|
312
|
+
import { openai } from "@ai-sdk/openai";
|
|
313
|
+
import { savingsMiddleware, getSavingsReport } from "@bivelio/savings-layer/ai-sdk";
|
|
314
|
+
|
|
315
|
+
const savings = savingsMiddleware({
|
|
316
|
+
licenseKey: process.env.BIVELIO_LICENSE_KEY,
|
|
317
|
+
licenseServerUrl: "https://savings.bivelio.com",
|
|
318
|
+
serviceAccountKey: process.env.BIVELIO_SERVICE_KEY,
|
|
319
|
+
readOnlyTools: ["get_weather"],
|
|
320
|
+
});
|
|
321
|
+
const model = wrapLanguageModel({ model: openai("gpt-4o-mini"), middleware: savings }); // last in the list
|
|
322
|
+
|
|
323
|
+
const result = await generateText({
|
|
324
|
+
model,
|
|
325
|
+
prompt: "What is 2 + 2 * 3?",
|
|
326
|
+
providerOptions: { bivelio: { context: { sessionId: "user-42" } } },
|
|
327
|
+
});
|
|
328
|
+
console.log(getSavingsReport(result)?.cost.basis); // "estimated": answered without calling the model
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
For `streamText`, read it with `getSavingsReport({ providerMetadata: await result.providerMetadata })`.
|
|
332
|
+
|
|
333
|
+
### Mastra
|
|
334
|
+
|
|
335
|
+
`pnpm add @bivelio/savings-layer @mastra/core` — an input processor for
|
|
336
|
+
`@mastra/core` 1.x:
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
import { Agent } from "@mastra/core/agent";
|
|
340
|
+
import { savingsProcessor } from "@bivelio/savings-layer/mastra";
|
|
341
|
+
|
|
342
|
+
const bivelio = savingsProcessor({
|
|
343
|
+
licenseKey: process.env.BIVELIO_LICENSE_KEY,
|
|
344
|
+
licenseServerUrl: "https://savings.bivelio.com",
|
|
345
|
+
serviceAccountKey: process.env.BIVELIO_SERVICE_KEY,
|
|
346
|
+
onReport: (r) => console.log(r.cost.basis),
|
|
347
|
+
});
|
|
348
|
+
const agent = new Agent({
|
|
349
|
+
id: "support",
|
|
350
|
+
name: "Support",
|
|
351
|
+
instructions: "Answer briefly.",
|
|
352
|
+
model: "openai/gpt-4o-mini",
|
|
353
|
+
inputProcessors: [bivelio], // last in the list
|
|
354
|
+
});
|
|
355
|
+
console.log((await agent.generate("What is 2 + 2 * 3?")).text); // "8"
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
It covers `generate()` and `stream()`, not `generateLegacy()`/`streamLegacy()` or a
|
|
359
|
+
durable agent that resumes in another process; identical requests in flight are not
|
|
360
|
+
shared here. `emitReportPart: true` also streams each report as a
|
|
361
|
+
`data-bivelio-savings-report` part.
|
|
362
|
+
|
|
363
|
+
### LangChain.js
|
|
364
|
+
|
|
365
|
+
`pnpm add @bivelio/savings-layer langchain @langchain/core @langchain/openai` — an agent
|
|
366
|
+
middleware for `createAgent` in `langchain` 1.x. The model string (`"openai:…"`) is loaded
|
|
367
|
+
through its integration package, so `@langchain/openai` (or `@langchain/anthropic`) has
|
|
368
|
+
to be installed:
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
import { createAgent } from "langchain";
|
|
372
|
+
import { savingsAgentMiddleware, getSavingsReportFromMessage } from "@bivelio/savings-layer/langchain";
|
|
373
|
+
|
|
374
|
+
const agentSavings = savingsAgentMiddleware({
|
|
375
|
+
licenseKey: process.env.BIVELIO_LICENSE_KEY,
|
|
376
|
+
licenseServerUrl: "https://savings.bivelio.com",
|
|
377
|
+
serviceAccountKey: process.env.BIVELIO_SERVICE_KEY,
|
|
378
|
+
});
|
|
379
|
+
const lcAgent = createAgent({ model: "openai:gpt-4o-mini", tools: [], middleware: [agentSavings] }); // last
|
|
380
|
+
const state = await lcAgent.invoke({ messages: [{ role: "user", content: "What is 2 + 2 * 3?" }] });
|
|
381
|
+
console.log(getSavingsReportFromMessage(state.messages.at(-1))?.cost.basis);
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
The model call is measured when it finishes; tokens you stream with
|
|
385
|
+
`streamMode: "messages"` keep coming from the model as usual. A chat model with its
|
|
386
|
+
own `cache` set is reported as `unmeasured`: its counter may be a cached copy.
|
|
387
|
+
|
|
388
|
+
### Anthropic TypeScript SDK
|
|
389
|
+
|
|
390
|
+
`pnpm add @bivelio/savings-layer @anthropic-ai/sdk` — a client middleware for
|
|
391
|
+
`@anthropic-ai/sdk` 0.103 or later:
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
395
|
+
import { anthropicSavings } from "@bivelio/savings-layer/anthropic-sdk";
|
|
396
|
+
|
|
397
|
+
const claudeSavings = anthropicSavings({
|
|
398
|
+
licenseKey: process.env.BIVELIO_LICENSE_KEY,
|
|
399
|
+
licenseServerUrl: "https://savings.bivelio.com",
|
|
400
|
+
serviceAccountKey: process.env.BIVELIO_SERVICE_KEY,
|
|
401
|
+
});
|
|
402
|
+
const client = new Anthropic({ middleware: [claudeSavings.middleware] }); // last in the list
|
|
403
|
+
const message = await client.messages.create({
|
|
404
|
+
model: "claude-opus-5",
|
|
405
|
+
max_tokens: 256,
|
|
406
|
+
messages: [{ role: "user", content: "What is 2 + 2 * 3?" }],
|
|
407
|
+
});
|
|
408
|
+
console.log(claudeSavings.reportFor(message)?.cost.basis);
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
It runs once per HTTP attempt, inside the SDK's retries: an attempt the provider
|
|
412
|
+
rejects reaches the SDK unchanged and is not recorded. Only message creation goes
|
|
413
|
+
through the layer; `countTokens`, batches, models and files pass untouched.
|
|
414
|
+
|
|
231
415
|
## What you get on every request
|
|
232
416
|
|
|
233
417
|
A `SavingsReport` with the numbers behind every claim, kept in **four separate
|
|
@@ -240,6 +424,29 @@ ledgers** that are never summed into one flattering figure:
|
|
|
240
424
|
| `compressed_tokens` | information kept, but encoded with fewer tokens |
|
|
241
425
|
| `provider_cached_tokens` | tokens still in the prompt, billed/processed more cheaply |
|
|
242
426
|
|
|
427
|
+
**Quality, kept apart from money.** `report.qualityLedger` records how the answer you
|
|
428
|
+
received earned its place: whether it came from your provider, a replay, or the answer
|
|
429
|
+
to a reworded question (with its similarity score); whether it was checked against your
|
|
430
|
+
output contract, repaired or cut off by the output limit; and what the model cascade
|
|
431
|
+
decided. It holds no money, is never added to the four ledgers, and never leaves your
|
|
432
|
+
process. Reusing answers to reworded questions is opt-in, and by default it covers
|
|
433
|
+
single-shot requests only: requests that declare tools, carry tool calls or results, or
|
|
434
|
+
hold more than one user turn are left out unless you set
|
|
435
|
+
`savings.semanticCache: "include-agentic"`.
|
|
436
|
+
|
|
437
|
+
**Calibrated reuse (opt-in).** A fixed similarity floor means something different for
|
|
438
|
+
every embedder and every workload, so you can make the cache earn its floor on your own
|
|
439
|
+
traffic instead: `new InMemorySemanticCache({ embed, calibration: { maxErrorRate: 0.02 } })`.
|
|
440
|
+
It then serves no reworded answer until your labels show, at 95 % confidence, that at
|
|
441
|
+
most 2 % of the answers it would reuse from some score up are wrong, and it re-tests that
|
|
442
|
+
floor as labels accumulate. Until then every candidate goes to your provider, exactly as
|
|
443
|
+
it would without the cache, and the fresh answer is compared with the stored one by your
|
|
444
|
+
`judge` (by default only identical answers count, which suits classification and
|
|
445
|
+
structured output; pass your own for free text). Each calibrated reuse carries its bound
|
|
446
|
+
and the labels behind it in `report.qualityLedger.cacheMatch.calibration`. Calibration
|
|
447
|
+
costs reuses, never an extra call, and it does not change how anything is billed. If you
|
|
448
|
+
already have labeled pairs, `calibrateSemanticFloor()` runs the same test offline.
|
|
449
|
+
|
|
243
450
|
And a promise you can hold it to: **never worse than your JSON.** Payload encodings
|
|
244
451
|
are chosen by counting real tokens with the destination model's own tokenizer, and
|
|
245
452
|
BV‑SALA only moves away from plain JSON when the saving clearly clears the bar —
|
|
@@ -295,6 +502,183 @@ and every pair as a CSV download.
|
|
|
295
502
|
|
|
296
503
|
</details>
|
|
297
504
|
|
|
505
|
+
<details>
|
|
506
|
+
<summary><b>Provider price levers (opt-in, off by default)</b></summary>
|
|
507
|
+
|
|
508
|
+
Two request flags ask your provider for a lower price on the same tokens. Neither
|
|
509
|
+
changes the answer, and what they are worth is reported next to the four ledgers:
|
|
510
|
+
never folded into `cost.*`, never part of any commission.
|
|
511
|
+
|
|
512
|
+
**`savings.deferrable`** asks for the provider's Flex tier: OpenAI, and Gemini through
|
|
513
|
+
its OpenAI‑compatible endpoint. Anthropic has no cheaper synchronous tier: its
|
|
514
|
+
discount lives only in Message Batches, which `layer.batches` (below) sends to.
|
|
515
|
+
Bedrock and DeepSeek off‑peak pricing are not supported.
|
|
516
|
+
|
|
517
|
+
- **Latency.** OpenAI publishes no latency commitment for Flex; Google publishes a
|
|
518
|
+
target of minutes. Keep deferrable requests off user‑facing paths.
|
|
519
|
+
`report.deferred.latency` quotes what the provider documents.
|
|
520
|
+
- **Risk.** Without capacity the provider refuses the request (429, or 503 on Gemini)
|
|
521
|
+
instead of waiting; OpenAI documents that the refusal is not charged. With
|
|
522
|
+
`priceLevers: { onCapacityError: "standard" }` the layer repeats it once at the
|
|
523
|
+
standard tier and price, and says so in `report.deferred.capacityFallback`.
|
|
524
|
+
- **What it is worth.** Your provider's usage counter for that call, priced at the two
|
|
525
|
+
published rate cards (standard, and the tier that actually served it), component
|
|
526
|
+
by component, with the date the prices were checked. It is `measured` only when
|
|
527
|
+
the served tier was read back from the provider's own API, the prices are list
|
|
528
|
+
prices and the counter says how much of the input was read from (or written to)
|
|
529
|
+
the prompt cache; otherwise `estimated`, and `basisReason` says why. It never
|
|
530
|
+
exceeds the difference between the two published rate cards, even with your own
|
|
531
|
+
`costModels`. A tier the provider did not confirm is never credited.
|
|
532
|
+
|
|
533
|
+
**`savings.standalone`** (GPT‑5.6 and later, on OpenAI's own API) declares that nothing
|
|
534
|
+
after the stable prefix of this request will be sent again. The provider then writes
|
|
535
|
+
its prompt cache only at the end of that prefix, or nowhere, instead of at the end of
|
|
536
|
+
your message, which on these models costs more than plain input. The trade‑off:
|
|
537
|
+
repeats and continuations of this exact prompt lose the cache reads the provider
|
|
538
|
+
default would give them, so leave it off conversation turns you will continue. Its
|
|
539
|
+
figure, `report.cacheWrites.avoidedWritePremium`, is always `estimated`: the provider
|
|
540
|
+
default was not run.
|
|
541
|
+
|
|
542
|
+
```ts
|
|
543
|
+
const leveredLayer = createSavingsLayer({
|
|
544
|
+
provider,
|
|
545
|
+
licenseKey: process.env.BIVELIO_LICENSE_KEY,
|
|
546
|
+
priceLevers: { deferrable: true, onCapacityError: "standard" }, // layer-wide defaults
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
const { report } = await leveredLayer.generate({
|
|
550
|
+
model: "openai/gpt-5.6-luna",
|
|
551
|
+
messages: [
|
|
552
|
+
{ role: "system", content: "…your long, fixed instructions…" },
|
|
553
|
+
{ role: "user", content: "Classify ticket 42." },
|
|
554
|
+
],
|
|
555
|
+
savings: { standalone: true }, // a request's own value always wins
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
report.deferred?.tierSaving; // { amount, basis, standardCost, appliedCost, pricesCheckedOn, basisReason }
|
|
559
|
+
report.cacheWrites; // { mode, breakpoints, avoidedWritePremium?, reason }
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
**`layer.batches`** sends requests that can wait hours to the provider's own batch API,
|
|
563
|
+
billed at its published batch rate card: Anthropic's Message Batches through
|
|
564
|
+
`AnthropicProvider`, and OpenAI's Batch API through `OpenAICompatibleProvider` on
|
|
565
|
+
`api.openai.com`. Gemini's batch mode is not supported yet.
|
|
566
|
+
|
|
567
|
+
- **What runs.** Each request is translated by the same provider adapter as
|
|
568
|
+
`generate()` and sent as you wrote it: the layer's own per‑request savings (caching,
|
|
569
|
+
compression, model cascade, retrieval) do not run on batches. The layer splits the
|
|
570
|
+
job at the provider's limits (and, on OpenAI, one model per file).
|
|
571
|
+
- **Latency.** The provider's: results arrive within its batch window, not on a
|
|
572
|
+
request/response path. `submit()` returns a plain‑JSON job you can store and read
|
|
573
|
+
from another process.
|
|
574
|
+
- **What it is worth.** Each result's usage counter at the standard and at the batch
|
|
575
|
+
published rate cards, component by component, with the same `measured` /
|
|
576
|
+
`estimated` rule as above. The tier is read from the provider: Anthropic reports it
|
|
577
|
+
on every result; on OpenAI it is the batch the result came from. Nothing is
|
|
578
|
+
metered and the batch saving is never billed.
|
|
579
|
+
|
|
580
|
+
```ts
|
|
581
|
+
const tickets = [{ id: "t-1", text: "My invoice is wrong." }]; // your deferrable work
|
|
582
|
+
|
|
583
|
+
const job = await layer.batches.submit(
|
|
584
|
+
tickets.map((t) => ({
|
|
585
|
+
customId: t.id,
|
|
586
|
+
request: { model: "openai/gpt-4o-mini", messages: [{ role: "user", content: t.text }] },
|
|
587
|
+
})),
|
|
588
|
+
);
|
|
589
|
+
// …later, even from another process (store `job` as JSON):
|
|
590
|
+
const results = await layer.batches.results(job);
|
|
591
|
+
results.ended; // false while the provider is still working
|
|
592
|
+
results.items; // one per request: { customId, outcome, text, usage, tierSaving?, reason }
|
|
593
|
+
results.summary; // { succeeded, errored, expired, credited, tierSaving, basis, pricesCheckedOn, reason, … }
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
</details>
|
|
597
|
+
|
|
598
|
+
<details>
|
|
599
|
+
<summary><b>Tool outputs, compacted where they are born (opt-in, off by default)</b></summary>
|
|
600
|
+
|
|
601
|
+
In an agent loop your history is re-sent on every turn and your provider reads it
|
|
602
|
+
from its prompt cache at a fraction of the input price. Rewriting anything it has
|
|
603
|
+
already cached turns everything after it back into full-price cache writes, which is
|
|
604
|
+
how "fewer tokens" ends up as a bigger bill. So this lever only ever changes a tool
|
|
605
|
+
output on the turn it arrives, and re-sends exactly the same bytes afterwards.
|
|
606
|
+
|
|
607
|
+
It removes terminal colour codes, superseded progress-bar states, whitespace between
|
|
608
|
+
JSON tokens and runs of identical lines (folded into one line plus a count), and
|
|
609
|
+
nothing else: no summaries, no dropped words. Each change either reverses exactly or
|
|
610
|
+
leaves what a terminal would display. Outputs of calls that name a file are left
|
|
611
|
+
verbatim, so an agent that edits by exact match still sees the file as it is.
|
|
612
|
+
|
|
613
|
+
```ts
|
|
614
|
+
const agentLayer = createSavingsLayer({
|
|
615
|
+
provider,
|
|
616
|
+
licenseKey: process.env.BIVELIO_LICENSE_KEY,
|
|
617
|
+
toolOutputCompaction: true, // or { exceptTools: ["read_logs"], minSavedChars: 512 }
|
|
618
|
+
});
|
|
619
|
+
// or per request: savings: { toolOutputCompaction: true }
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
- **Switch it on for whole conversations.** The decision depends only on each
|
|
623
|
+
output's own text, so every process and every turn produces the same bytes. A
|
|
624
|
+
conversation that started without it is rewritten once, on its first compacted
|
|
625
|
+
request.
|
|
626
|
+
- **Counted, not billed.** What it removes shows in `report.tokens.compressedInput`
|
|
627
|
+
and in `report.traces`; it never enters `cost.*` or any commission. Your provider's
|
|
628
|
+
invoice shows the effect.
|
|
629
|
+
- **Inside a framework,** call `compactToolOutput(text)` in your tool before you
|
|
630
|
+
return its result: your history then holds the compacted text itself.
|
|
631
|
+
- **Measure it by conversation, not by request.** A verification copy re-sends one
|
|
632
|
+
request with the original history, which your provider has not cached, so on
|
|
633
|
+
turns that re-send compacted outputs the copy costs more than going without the
|
|
634
|
+
layer would have. Compare whole conversations with and without it.
|
|
635
|
+
|
|
636
|
+
</details>
|
|
637
|
+
|
|
638
|
+
<details>
|
|
639
|
+
<summary><b>Prompt-cache breakpoints placed from your traffic (opt-in)</b></summary>
|
|
640
|
+
|
|
641
|
+
By default the layer writes the provider's prompt cache at two fixed places: the end
|
|
642
|
+
of the system header and the end of the history before your last user message, and
|
|
643
|
+
stops marking a prefix whose measured hit rate does not pay the write premium.
|
|
644
|
+
|
|
645
|
+
`cacheBreakpoints: "auto"` places them from what your traffic actually does. The layer
|
|
646
|
+
compares each request's prefix with the ones it sent before, per model, tool set and
|
|
647
|
+
system header, and learns how often each part is sent again and after how long. That
|
|
648
|
+
costs nothing: it compares locally and marks nothing to find out. Once the evidence
|
|
649
|
+
is conclusive:
|
|
650
|
+
|
|
651
|
+
- **Tail.** When requests continue each other (agent tool loops, conversations), it
|
|
652
|
+
also marks the last block, so the next turn reads the whole prompt from cache.
|
|
653
|
+
Tool results arrive after your last user message, so the fixed placement never
|
|
654
|
+
caches them.
|
|
655
|
+
- **History.** When the history before the question is not sent again (one-off
|
|
656
|
+
requests with their own document), it moves the breakpoint back to the system
|
|
657
|
+
header, so no write premium is paid on a prefix nobody reads.
|
|
658
|
+
- **One-hour lifetime** (Anthropic). When turns come back after more than five
|
|
659
|
+
minutes but within the hour, and the size of the request makes the 2x write pay.
|
|
660
|
+
- **Explicit mode** (GPT‑5.6 and later, on OpenAI's own API). When continuations are
|
|
661
|
+
rare, your question is not written to the cache. `savings.standalone` still wins
|
|
662
|
+
when you set it, and `report.cacheWrites` says when the layer chose it.
|
|
663
|
+
|
|
664
|
+
Until then it behaves exactly like the default. Every decision and its reason is in
|
|
665
|
+
`report.traces` (`prefix-cache-placement`), and the effect is read from the provider's
|
|
666
|
+
own counters: if they do not confirm the cache reads the layer predicted, that
|
|
667
|
+
traffic goes back to the default placement. The write premium it causes, one-hour
|
|
668
|
+
writes at 2x included, is charged to `cost.actual`; the cache discount it earns is
|
|
669
|
+
counted in ledger 4 like any provider cache read, never credited as the layer's
|
|
670
|
+
saving and never part of any commission.
|
|
671
|
+
|
|
672
|
+
```ts
|
|
673
|
+
const placedLayer = createSavingsLayer({
|
|
674
|
+
provider, // Anthropic, or OpenAI GPT-5.6+ on OpenAI's own API
|
|
675
|
+
licenseKey: process.env.BIVELIO_LICENSE_KEY,
|
|
676
|
+
cacheBreakpoints: "auto",
|
|
677
|
+
});
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
</details>
|
|
681
|
+
|
|
298
682
|
<details>
|
|
299
683
|
<summary><b>Structured output — validated <code>data</code>, not just text</b></summary>
|
|
300
684
|
|
|
@@ -366,6 +750,185 @@ if (res.toolCalls?.length) {
|
|
|
366
750
|
A tool‑call turn is never served from cache and never counted as an avoided call:
|
|
367
751
|
an action that has not run yet cannot be a saving.
|
|
368
752
|
|
|
753
|
+
**Large catalogues: let the provider search them (opt‑in).** With hundreds of tools
|
|
754
|
+
(several MCP servers, say), `savings: { toolSearch: "auto" }` sends every tool deferred
|
|
755
|
+
behind the provider's own tool search instead of letting the layer's selector keep a
|
|
756
|
+
few. A catalogue that declares no `intents` or `risk` gives the selector nothing to go
|
|
757
|
+
on, so it keeps the first ones and the model cannot call the rest; with the provider's
|
|
758
|
+
search the model looks up what it needs, no tool you declared is out of reach, and the
|
|
759
|
+
cached prompt prefix stays the same from one question to the next.
|
|
760
|
+
|
|
761
|
+
- **Where.** The Anthropic adapter, on models in Anthropic's tool search compatibility
|
|
762
|
+
table. OpenAI documents its tool search for the Responses and Agents APIs, not for
|
|
763
|
+
Chat Completions, which is what this SDK's OpenAI adapter speaks, so OpenAI requests
|
|
764
|
+
keep the selector.
|
|
765
|
+
- **When.** `"auto"` applies it when the adapter talks to Anthropic's own API, the
|
|
766
|
+
catalogue is over the provider's documented size, and deferring it pays for one
|
|
767
|
+
search on every request even with a warm prompt cache. `"always"` skips those checks
|
|
768
|
+
(for example behind a proxy that forwards the search blocks). Requests with an output
|
|
769
|
+
contract, or with the model cascade on, keep the selector.
|
|
770
|
+
- **Whose saving.** The provider's. Your usage counter shows it; the layer credits
|
|
771
|
+
nothing for it and charges no commission on it.
|
|
772
|
+
|
|
773
|
+
```ts
|
|
774
|
+
import { AnthropicProvider } from "@bivelio/savings-layer";
|
|
775
|
+
|
|
776
|
+
const claude = createSavingsLayer({
|
|
777
|
+
provider: new AnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY }),
|
|
778
|
+
licenseKey: process.env.BIVELIO_LICENSE_KEY,
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
const mcpTools: ToolDescriptor[] = []; // every tool your MCP servers list
|
|
782
|
+
|
|
783
|
+
const { report: searched } = await claude.generate({
|
|
784
|
+
model: "anthropic/claude-opus-5-5",
|
|
785
|
+
messages: [{ role: "user", content: "Which orders from service 17 are still open?" }],
|
|
786
|
+
tools: mcpTools,
|
|
787
|
+
savings: { toolSearch: { mode: "auto", alwaysLoaded: ["read_file"] } },
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
// Which path ran and why, and how many searches the provider ran.
|
|
791
|
+
console.log(searched.traces.filter((t) => t.stage === "tool-search"));
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
</details>
|
|
795
|
+
|
|
796
|
+
<details>
|
|
797
|
+
<summary><b>Your ledgers in your tracing — OpenTelemetry, Langfuse, Datadog</b></summary>
|
|
798
|
+
|
|
799
|
+
Wrap the layer once and every request becomes a span on the OpenTelemetry pipeline
|
|
800
|
+
you already run (`@opentelemetry/api` is an optional peer; the SDK never imports it):
|
|
801
|
+
|
|
802
|
+
```ts
|
|
803
|
+
import { trace } from "@opentelemetry/api";
|
|
804
|
+
import { instrumentSavingsLayer } from "@bivelio/savings-layer";
|
|
805
|
+
|
|
806
|
+
// One span per request, on the OpenTelemetry pipeline you already run.
|
|
807
|
+
const traced = instrumentSavingsLayer(layer, { tracer: trace });
|
|
808
|
+
const { report: tracedReport } = await traced.generate({
|
|
809
|
+
model: "openai/gpt-4o-mini",
|
|
810
|
+
messages: [{ role: "user", content: "What is 2 + 2 * 3?" }],
|
|
811
|
+
});
|
|
812
|
+
console.log(tracedReport.cost.basis); // the span carries the same figures
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
- **Standard spans, plus your ledgers.** Spans follow the
|
|
816
|
+
[GenAI semantic conventions](https://github.com/open-telemetry/semantic-conventions/blob/v1.41.1/model/gen-ai/registry.yaml)
|
|
817
|
+
as last published (OpenTelemetry semantic conventions v1.41.1): `chat {model}`,
|
|
818
|
+
kind CLIENT, provider, model and usage from your provider's counter (0 on an avoided
|
|
819
|
+
call: no token was billed). On top, `com.bivelio.*` carries the four ledgers, the
|
|
820
|
+
`basis`, the cost and the ids; `com.bivelio.request_id` is the same id as your usage
|
|
821
|
+
record. Same results, same errors: `traced` hands back the very objects `layer` does.
|
|
822
|
+
- **Money is kept apart by basis.** The money saved is
|
|
823
|
+
`com.bivelio.cost.saved.measured`, `.reference` or `.estimated` — one key per span,
|
|
824
|
+
the one of its basis — so a sum over one key never mixes measured and estimated
|
|
825
|
+
money; `unmeasured` spans carry counts and no money. `report.cost.netSavings` is a
|
|
826
|
+
ratio and goes out as `com.bivelio.cost.net_savings_ratio`; the money is
|
|
827
|
+
`baselineEstimate − actual`, and that is what `saved.*` holds.
|
|
828
|
+
- **None of this talks to BiVelio.** Spans go to your exporter; files stay where you
|
|
829
|
+
write them. Your prompts, completions and provider keys never reach BiVelio, and no
|
|
830
|
+
span carries them either: ids, counts, money and enums only, and a failed request
|
|
831
|
+
records its `error.type`, never its message.
|
|
832
|
+
- **Langfuse.** Add the `LangfuseSpanProcessor` from `@langfuse/otel` to your tracer
|
|
833
|
+
provider. These spans carry `gen_ai.*`, so its default filter exports them
|
|
834
|
+
([Langfuse OpenTelemetry docs](https://langfuse.com/integrations/native/opentelemetry)).
|
|
835
|
+
`instrumentSavingsLayer(layer, { tracer: trace, langfuse: { metadata: true } })`
|
|
836
|
+
makes the figures filterable metadata, and `langfuse: { cost: true }` hands Langfuse
|
|
837
|
+
the report's own cost (in USD) instead of its estimate. Tracing with `@langfuse/tracing` by
|
|
838
|
+
hand? Pass `toLangfuseGeneration(report)` to `generation.update()`.
|
|
839
|
+
- **Datadog LLM Observability** reads these spans over OTLP
|
|
840
|
+
([Datadog OpenTelemetry docs](https://docs.datadoghq.com/llm_observability/instrumentation/otel_instrumentation/)):
|
|
841
|
+
|
|
842
|
+
```bash
|
|
843
|
+
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf
|
|
844
|
+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=<your Datadog site's OTLP traces endpoint>
|
|
845
|
+
OTEL_EXPORTER_OTLP_TRACES_HEADERS=dd-api-key=<your Datadog API key>,dd-otlp-source=llmobs
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
The `com.bivelio.*` attributes arrive as tags; with `datadog: { metadata: true }`
|
|
849
|
+
they also arrive as span metadata. Datadog drops spans that carry no `gen_ai.*`
|
|
850
|
+
attribute, so use `instrumentSavingsLayer` there rather than `recordSavingsOnSpan`
|
|
851
|
+
(which writes `com.bivelio.*` on a span you already have, and no `gen_ai.*` unless
|
|
852
|
+
you ask for it with `genAi: true`).
|
|
853
|
+
|
|
854
|
+
| Attribute | What it holds |
|
|
855
|
+
| --- | --- |
|
|
856
|
+
| `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) |
|
|
857
|
+
| `gen_ai.usage.input_tokens`, `.output_tokens`, `.cache_read.input_tokens` | your provider's counter; absent when it reported no usage |
|
|
858
|
+
| `com.bivelio.cost.basis`, `.currency`, `.baseline`, `.actual` | where the money comes from, its currency (USD by default), and both sides |
|
|
859
|
+
| `com.bivelio.cost.saved.measured` \| `.reference` \| `.estimated` | the money saved, in the key of its basis only |
|
|
860
|
+
| `com.bivelio.cost.net_savings_ratio` | `cost.netSavings`, a 0..1 ratio — not money |
|
|
861
|
+
| `com.bivelio.calls.*`, `com.bivelio.tokens.*` | the four ledgers: avoided calls, eliminated, compressed and provider‑cached tokens |
|
|
862
|
+
| `com.bivelio.request_id`, `.model.id`, `.cache.result`, `.quality.status` | ids and outcomes |
|
|
863
|
+
|
|
864
|
+
</details>
|
|
865
|
+
|
|
866
|
+
<details>
|
|
867
|
+
<summary><b>Your ledgers in your FinOps tool — FOCUS rows and a daily savings series</b></summary>
|
|
868
|
+
|
|
869
|
+
Keep each report with the time it came back, and export whenever you like:
|
|
870
|
+
|
|
871
|
+
```ts
|
|
872
|
+
import { writeFileSync } from "node:fs";
|
|
873
|
+
import { toFocusRows, toSavingsSeries, toCsv, type SavingsRecord } from "@bivelio/savings-layer";
|
|
874
|
+
|
|
875
|
+
const records: SavingsRecord[] = [];
|
|
876
|
+
const answer = await layer.generate({
|
|
877
|
+
model: "openai/gpt-4o-mini",
|
|
878
|
+
messages: [{ role: "user", content: "What is 2 + 2 * 3?" }],
|
|
879
|
+
});
|
|
880
|
+
records.push({ at: Date.now(), report: answer.report }); // keep reports as they come back
|
|
881
|
+
|
|
882
|
+
// FOCUS 1.4 rows, one per day, model and basis. Every cost column is 0 by design:
|
|
883
|
+
// your provider's bill already has the spend, so these rows add savings, never cost.
|
|
884
|
+
const rows = toFocusRows(records, { billingAccountId: "org-XXXXXXXX" }); // your provider billing account id
|
|
885
|
+
writeFileSync("bv-sala-focus.csv", toCsv(rows));
|
|
886
|
+
|
|
887
|
+
// A daily series for tools that take savings as a metric: one file per basis.
|
|
888
|
+
const measured = toSavingsSeries(records).filter((r) => r.measuredSavings !== null);
|
|
889
|
+
writeFileSync(
|
|
890
|
+
"vantage-measured-savings.csv",
|
|
891
|
+
toCsv(measured.map((r) => ({ date: r.date, amount: r.measuredSavings, model: r.model }))),
|
|
892
|
+
);
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
The rows follow [FOCUS 1.4](https://github.com/FinOps-Open-Cost-and-Usage-Spec/FOCUS_Spec/tree/v1.4),
|
|
896
|
+
the FinOps Open Cost and Usage Specification: `ChargeCategory` is `Adjustment`, the
|
|
897
|
+
service provider is the one in the model id (`OpenAI`, `Anthropic`…; pass
|
|
898
|
+
`serviceProviderName`, `invoiceIssuerName` or `hostProviderName` when a cloud or a
|
|
899
|
+
gateway bills you), and no SKU or unit price is invented. Nothing is sent anywhere:
|
|
900
|
+
`toFocusRows`, `toSavingsSeries` and `toCsv` are plain functions over the reports you
|
|
901
|
+
kept. Where each file goes, as each tool documents it in September 2026:
|
|
902
|
+
|
|
903
|
+
- **A FOCUS data lake or warehouse** reads every column, the `x_BiVelio*` ones included.
|
|
904
|
+
- **[Finout](https://docs.finout.io/get-started-with-finout/cost-and-usage-types/focus-in-finout)**
|
|
905
|
+
loads a FOCUS CSV as a custom cost center; you send the file to Finout support.
|
|
906
|
+
- **[Vantage](https://docs.vantage.sh/connecting_custom_providers)** Custom Providers
|
|
907
|
+
accept FOCUS 1.1–1.3 files but use only a few columns for cost reporting, and say
|
|
908
|
+
nothing about `x_` columns, so the savings would not show. Upload the series as a
|
|
909
|
+
[Business Metric](https://docs.vantage.sh/per_unit_costs) instead
|
|
910
|
+
(`date,amount,<labels>`), one file per basis, so a metric never mixes bases.
|
|
911
|
+
Vantage rejects two rows with the same date and labels: with `dimensions`, add
|
|
912
|
+
them as label columns too.
|
|
913
|
+
- **[CloudZero](https://docs.cloudzero.com/docs/anycost-common-bill-format-cbf)** AnyCost
|
|
914
|
+
takes its own Common Bill Format; the series can go in as a
|
|
915
|
+
[Telemetry Stream](https://docs.cloudzero.com/docs/telemetry-streams).
|
|
916
|
+
|
|
917
|
+
The custom columns, in the currency you pass (`currency`, USD by default):
|
|
918
|
+
|
|
919
|
+
| Column | What it holds |
|
|
920
|
+
| --- | --- |
|
|
921
|
+
| `x_BiVelioBasis`, `x_BiVelioModelId` | the basis of every figure in the row, and the model id |
|
|
922
|
+
| `x_BiVelioRequestCount`, `x_BiVelioProviderCallCount`, `x_BiVelioAvoidedCallCount` | requests, calls that reached the provider, calls avoided |
|
|
923
|
+
| `x_BiVelioBaselineCost`, `x_BiVelioActualCost` | both sides of the saving; empty when the basis is `unmeasured` |
|
|
924
|
+
| `x_BiVelioMeasuredSavings`, `x_BiVelioReferenceSavings`, `x_BiVelioEstimatedSavings` | `baseline − actual`, in the column of the row's basis only; negative when the layer cost you money |
|
|
925
|
+
| `x_BiVelioEliminatedInputFloorCost` | the separate floor lane for context your provider cleared |
|
|
926
|
+
| `x_BiVelio…InputTokens`, `x_BiVelio…OutputTokens` | the token ledgers: baseline, actual, eliminated, compressed, provider‑cached |
|
|
927
|
+
|
|
928
|
+
`Tags` carries `bivelio/basis` and `bivelio/model`, plus any tags you return from the
|
|
929
|
+
`tags` option. Sum `measured` and `reference` for what you can check against your
|
|
930
|
+
provider invoice; `estimated` has its own column on purpose.
|
|
931
|
+
|
|
369
932
|
</details>
|
|
370
933
|
|
|
371
934
|
<details>
|
|
@@ -390,6 +953,10 @@ BVSALA_SEAT_ID=worker-03 # stable per replica; [A-Za-z0-9._:/-], max 128 chars
|
|
|
390
953
|
|
|
391
954
|
- **Node ≥ 20.** The package is **ESM‑only** (`"type": "module"`); from CommonJS,
|
|
392
955
|
`require()` works on Node ≥ 22.
|
|
956
|
+
- Framework adapters: `ai` 6 or 7, `@mastra/core` ≥ 1.68 < 2, `langchain` ≥ 1.1 < 2
|
|
957
|
+
with `@langchain/core` ≥ 1.1 < 2, and `@anthropic-ai/sdk` ≥ 0.103 < 1 — optional
|
|
958
|
+
peers, needed only for the adapter you import. `ai` 7 and `@mastra/core` declare
|
|
959
|
+
Node ≥ 22 in their own `engines`.
|
|
393
960
|
- Optional peer `gpt-tokenizer` for exact OpenAI token counts, loaded automatically
|
|
394
961
|
when installed — without it a built‑in estimator is used. It improves the accuracy
|
|
395
962
|
of estimated figures; what reaches your invoice is `measured` and `reference`, which
|
|
@@ -402,14 +969,20 @@ BVSALA_SEAT_ID=worker-03 # stable per replica; [A-Za-z0-9._:/-], max 128 chars
|
|
|
402
969
|
|
|
403
970
|
## Licensing & privacy
|
|
404
971
|
|
|
405
|
-
BV‑SALA is commercial
|
|
406
|
-
year**, EUR; VAT not included)
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
972
|
+
BV‑SALA is commercial software: a per‑seat subscription (**€4 / seat / month** or
|
|
973
|
+
**€39 / seat / year**, EUR; VAT not included), paid whether or not you save, plus a
|
|
974
|
+
tiered commission on measured savings only, as described at the top of this page, in
|
|
975
|
+
[LICENSE](./LICENSE) §6 and in the Published Pricing it refers to,
|
|
976
|
+
**[savings.bivelio.com/pricing](https://savings.bivelio.com/pricing)**. No free tier and no trial: what you can check before
|
|
977
|
+
paying is the published measurement. The license is a signed token your SDK and the
|
|
978
|
+
gateway verify **offline**; the managed control plane and the revocation kill‑switch
|
|
979
|
+
are served from BiVelio. Your LLM traffic goes **straight to your provider** —
|
|
980
|
+
BiVelio receives per‑request usage (counts, cost, the model id and opaque ids), never
|
|
981
|
+
prompts, completions or provider keys; [LICENSE](./LICENSE) §5 lists every field, and
|
|
982
|
+
**[savings.bivelio.com/privacy](https://savings.bivelio.com/privacy)** says how long
|
|
983
|
+
each one is kept.
|
|
984
|
+
|
|
985
|
+
Get a key at **[savings.bivelio.com](https://savings.bivelio.com)**.
|
|
413
986
|
Onboarding & general questions: **support@bivelio.com** · licensing & compliance:
|
|
414
987
|
**privacy@bivelio.com** · technical: **team@bivelio.com**.
|
|
415
988
|
|