@bivelio/savings-layer 0.4.24 → 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.
@@ -8,15 +8,19 @@ terminal). Cada afirmación va marcada **[V]** (verificada en fuente) o **[I]**
8
8
  ## Qué es esto
9
9
 
10
10
  `bvsala gateway` es un proxy local medidor: escucha en `127.0.0.1`, reenvía
11
- cada petición **byte a byte** al proveedor (`https://api.anthropic.com` por
12
- defecto) y, una vez servida la respuesta, lee el `usage` del stream y lo
13
- convierte en coste. Tus prompts jamás salen de tu máquina hacia BiVelio — solo
14
- cuentas y coste. Es fail-closed como el SDK: arranca solo con tu clave de
15
- servicio (`BIVELIO_SERVICE_KEY`) y una licencia BiVelio activa, que recoge con
16
- esa clave y cuya firma verifica en tu máquina. Una vez arrancado, jamás corta
17
- una petición en curso: si la licencia caduca o se revoca, solo las peticiones
18
- NUEVAS reciben un HTTP 402 hasta renovarla. La medición es best-effort: con la
19
- ingesta caída, reenvía y avisa.
11
+ cada petición al proveedor (`https://api.anthropic.com` por defecto) —**byte a
12
+ byte**, salvo las palancas que se explican abajo (con licencia PRO, la de tarifa
13
+ va encendida por defecto)— y, una vez servida la respuesta, lee el `usage` del
14
+ stream y lo convierte en coste. Ni tus prompts, ni las respuestas, ni la clave de
15
+ tu proveedor salen de tu máquina hacia BiVelio: solo viaja el uso de cada
16
+ petición —cuentas de tokens, coste, el id del modelo, un id de petición y los ids
17
+ opacos de asiento y de sesión—, y la lista exacta de campos está en la
18
+ LICENSE §5. Es fail-closed como el SDK: arranca solo con tu clave de servicio
19
+ (`BIVELIO_SERVICE_KEY`) y una licencia BiVelio activa, que recoge con esa clave y
20
+ cuya firma verifica en tu máquina. Una vez arrancado, jamás corta una petición en
21
+ curso: si la licencia caduca o se revoca, solo las peticiones NUEVAS reciben un
22
+ HTTP 402 hasta renovarla. La medición es best-effort: con la ingesta caída,
23
+ reenvía y avisa.
20
24
 
21
25
  Mide cada petición. Con licencia PRO aplica por defecto **una sola palanca**:
22
26
  rebaja el TTL de escritura de caché mientras la sesión sigue caliente — cambia la
@@ -205,3 +209,107 @@ no se promociona ni se documenta como receta.
205
209
  di eso, nunca «aprobado por Anthropic». El contrato del gateway es una lista
206
210
  abierta de cabeceras y campos que crece con cada release del terminal; el
207
211
  passthrough byte a byte es lo que lo mantiene vivo sin tocar nada.
212
+
213
+ ## What the gateway guarantees: it forwards what your client sends
214
+
215
+ A proxy in front of your provider can raise your bill without breaking anything
216
+ you can see: a lost cache marker, a dropped beta header, a block it does not
217
+ recognise. This gateway is built not to do that, and a test suite checks it
218
+ against a fake upstream on 127.0.0.1: `test/gateway-no-empeora.test.ts`, which
219
+ runs in our CI on every pull request and every push to `main`. The source
220
+ repository is not public, so this section lists exactly what the suite checks.
221
+
222
+ **With no lever armed, your provider receives what your client sent, byte for
223
+ byte:**
224
+
225
+ - **The request body**, including `tool_reference` blocks (from the provider's
226
+ tool search and from a client-side search tool such as Claude Code's), tools
227
+ with `defer_loading`, `cache_control` in every place the API reads it (top
228
+ level, `system`, `tools`, and text, image, document, `tool_use` and
229
+ `tool_result` blocks, including blocks nested inside a `tool_result` or inside
230
+ a document's `source.content`), each with its `ttl: "1h"`, the advisor tool
231
+ with its `advisor_tool_result` blocks, thinking signatures, and fields the
232
+ gateway does not know. Not a JSON equivalent: the same bytes.
233
+ - **The path and query string** (`/v1/messages?beta=true`).
234
+ - **Every request header**, including every `anthropic-beta` value (the 1-hour
235
+ cache TTL, the advisor, context management, OAuth, and betas that do not exist
236
+ yet). Three kinds stay on the hop from your client to the gateway, as HTTP
237
+ requires: hop-by-hop headers (RFC 7230 §6.1, plus `expect` and any header named
238
+ in `Connection`), `host` and `content-length`.
239
+ - **The response**: the same bytes and headers, except the transport ones
240
+ (`content-encoding`, `content-length`, `transfer-encoding`), because the
241
+ gateway receives the body already decompressed.
242
+
243
+ "No lever armed" means any of: a license without that lever's feature,
244
+ `BVSALA_GATEWAY_ELISION=0`, or a lever you have not turned on (the sibling
245
+ model, per-turn effort, the context-editing dials and usage injection are off by
246
+ default). The suite checks each of these with the request exactly as Claude
247
+ Code sends it (compact JSON, which every armed lever would rewrite) and with a
248
+ hand-formatted one, and next to each it runs the same setup with the lever armed
249
+ and checks that the request does change, so a lever that stopped switching off
250
+ would fail it.
251
+
252
+ **With a lever armed, the only change is the one it declares:**
253
+
254
+ | Lever | Applies to | What changes | What does not |
255
+ |---|---|---|---|
256
+ | Cache TTL (`cache_ttl_tuning`, on by default with PRO) | `/v1/messages` requests where a large part of the prompt is new (session start, compaction); a chained turn travels untouched | every `cache_control` with `ttl: "1h"` becomes `"5m"`, the top-level one included (a 1-hour top-level marker behind 5-minute block markers would be a 400) | a key named `cache_control` inside a tool call's `input` or a tool's `input_schema` is content and stays as it is; so does the advisor's `caching`; headers are untouched |
257
+ | Per-turn effort (`BVSALA_GATEWAY_ESFUERZO=low…max`) | `/v1/messages` for `claude-fable-5-1`, `claude-mythos-5-1` and `claude-opus-5`, when the request carries no `output_config` of its own (top level or in a message) | one message appended at the end of `messages`, `{"role":"system","content":[],"output_config":{"effort":…}}`, and `mid-conversation-output-config-2026-07-01` appended after your `anthropic-beta` value | your `anthropic-beta` value is kept verbatim in front; nothing earlier in `messages` moves |
258
+ | Sibling model (`BVSALA_GATEWAY_MODELO_HERMANO=1`) | `/v1/messages` and `/v1/messages/count_tokens` for `claude-fable-5`, except with a forced `tool_choice` or the 1M-context beta | `model` becomes `claude-fable-5-1` | headers are untouched |
259
+ | Context-editing dials (`BVSALA_GATEWAY_DIALES_ELISION=1`; measured as a net loss, off) | `/v1/messages` | your `clear_tool_uses_20250919` edit in `context_management` is replaced by ours | your other edits are kept, `clear_thinking_20251015` first; your `anthropic-beta` value is kept verbatim, with `context-management-2025-06-27` appended if it was not there |
260
+ | Usage in Chat Completions streams (`BVSALA_GATEWAY_USAGE_INJECT=1`, off) | streaming `…/chat/completions` requests without `stream_options` of their own | `"stream_options":{"include_usage":true},` inserted as text right after the opening `{` | the rest of the body, byte for byte (nothing is re-serialized); headers are untouched; a 400 or 422 gets your original request once more |
261
+
262
+ A lever that changes the body parses it and writes it back: your provider reads
263
+ the same JSON values, although whitespace and the way strings are escaped may
264
+ differ. When writing it back would change anything else (a number JavaScript
265
+ prints differently, such as `1.0`, `1e5`, `-0` or an integer beyond 2^53; an
266
+ object key that looks like an array index, which JavaScript moves to the front;
267
+ a key repeated in the same object; invalid UTF-8; a compressed body), no lever
268
+ is applied and the request travels byte for byte. If the provider answers 400 to
269
+ a request a lever changed, the gateway sends your original request once more,
270
+ unchanged, and meters that one.
271
+
272
+ **Degraded traffic.** The gateway also watches, with the provider's own usage
273
+ counters, for requests that come out worse than your client asked for:
274
+
275
+ | Log line: `[gateway] degraded traffic (session …) · …` | What it means |
276
+ |---|---|
277
+ | `prompt caching: … no cache read and no cache write` | the request marked `cache_control` at its end (top level or in the last message), the provider counted at least 4,096 input tokens (the largest minimum cacheable prompt in [Anthropic's prompt caching documentation](https://platform.claude.com/docs/en/build-with-claude/prompt-caching), checked on 2026-09-27) and reported neither a cache read nor a cache write |
278
+ | `cache TTL: … wrote the cache at the 5-minute TTL only` | every `cache_control` asked for `ttl: "1h"`, the TTL lever did not touch the request, and the provider wrote only 5-minute entries |
279
+ | `tool search: … tool search is off` | Claude Code traffic (a `x-claude-code-session-id` header or a `claude-cli/` user agent) sent at least 40 full tool definitions, none with `defer_loading` and no `tool_reference`. Claude Code turns tool search off by default behind any proxy, this gateway included: `bvsala claude` turns it back on in a new window, or export `ENABLE_TOOL_SEARCH=true` |
280
+
281
+ Each kind is logged once per session (Claude Code's session id; once per gateway
282
+ process for clients that send none) and every case is counted. `bvsala` prints
283
+ the totals when it exits, because the per-request log is silent inside `bvsala
284
+ claude|codex|goose|qwen` (unless `BVSALA_GATEWAY_LOG=1`). None of this is sent to BiVelio: the usage record has
285
+ no field for it (LICENSE §5), so it stays on your machine.
286
+
287
+ **Limits.**
288
+
289
+ - The guarantee covers what the gateway sends. It cannot make the provider
290
+ cache: the watcher reports, it does not fix.
291
+ - The cache TTL lever changes the price of a cache write, never the content the
292
+ model reads, and it is a trade: if a 5-minute entry expires before the next
293
+ turn starts, that turn pays for a cache write that a 1-hour entry would have
294
+ saved. The lever only acts where a large part of the prompt is being written
295
+ anyway (session start, compaction), and `BVSALA_GATEWAY_ELISION=0` turns it
296
+ off.
297
+ - On a request that declares server tools (web search, web fetch, code
298
+ execution, tool search, the advisor, MCP servers; the gateway counts any typed
299
+ tool other than a client tool such as bash or the text editor), the cache TTL
300
+ lever still applies but its saving is recorded as zero. When Claude uses a
301
+ server tool, the provider caches its result at the 5-minute TTL on its own,
302
+ whatever TTL your markers ask for, and reports that write in the same
303
+ `ephemeral_5m_input_tokens` counter
304
+ ([Anthropic's documentation](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-use-with-prompt-caching#server-tool-results-are-cached-automatically),
305
+ checked on 2026-09-27), so the invoice cannot tell which part the lever wrote.
306
+ - The two cache checks need the provider's usage: a request that is not metered
307
+ is not checked. The prompt-caching check uses the largest documented minimum
308
+ for every model, so a future model with a larger minimum would raise false
309
+ alarms until the threshold moves.
310
+ - For the same reason, the TTL check skips requests that declare server tools.
311
+ - The tool-search check only looks at Claude Code traffic: another client with
312
+ many tools may never have used tool search, and that is not degraded traffic.
313
+ - Header names arrive in lower case (HTTP header names are case-insensitive),
314
+ and a header your client sends twice arrives once, with both values joined by
315
+ a comma, in order.
@@ -117,8 +117,13 @@ con cada release de Claude Code; BV-SALA repite el smoke test de `bvsala claude`
117
117
  contra cada versión nueva.
118
118
 
119
119
  Las cabeceras `x-claude-code-session-id` / `agent-id` / `parent-agent-id`
120
- llegan en el tráfico real y hoy se reenvían sin consumirlas: el coste por sesión
121
- y por subagente está previsto en S3 §1.3, todavía no en el panel.
120
+ llegan en el tráfico real y se reenvían al proveedor sin tocarlas. Además, el
121
+ gateway LEE las dos primeras y, si tienen forma de identificador, adjunta su
122
+ valor (`sessionId`, `agentId`) al registro de uso que manda a BiVelio: es lo que
123
+ reparte el coste por sesión y por subagente en el panel. Son los ids que Claude
124
+ Code ya pone en sus propias peticiones, no una persona ni un contenido;
125
+ `parent-agent-id` no se lee. La lista completa de lo que viaja está en la
126
+ LICENSE §5.
122
127
 
123
128
  ## Nota BYOK y wording
124
129
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivelio/savings-layer",
3
- "version": "0.4.24",
3
+ "version": "0.4.25",
4
4
  "description": "The savings layer for your LLM bill: a library inside your code that lets through only what has to be paid for and returns a SavingsReport per call, measured with your provider's own meter and kept apart from estimates. Commercial license, paid per seat, commission only on measured savings. Your prompts never leave your infrastructure.",
5
5
  "keywords": [
6
6
  "llm",
@@ -15,6 +15,10 @@
15
15
  "email": "support@bivelio.com",
16
16
  "url": "https://savings.bivelio.com"
17
17
  },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/BiVelio/bivelio-savings-layer.git"
21
+ },
18
22
  "license": "SEE LICENSE IN LICENSE",
19
23
  "author": "BiVelio Inc.",
20
24
  "type": "module",
@@ -27,6 +31,26 @@
27
31
  "import": "./dist/index.js",
28
32
  "default": "./dist/index.js"
29
33
  },
34
+ "./ai-sdk": {
35
+ "types": "./dist/adapters/ai-sdk.d.ts",
36
+ "import": "./dist/adapters/ai-sdk.js",
37
+ "default": "./dist/adapters/ai-sdk.js"
38
+ },
39
+ "./mastra": {
40
+ "types": "./dist/adapters/mastra.d.ts",
41
+ "import": "./dist/adapters/mastra.js",
42
+ "default": "./dist/adapters/mastra.js"
43
+ },
44
+ "./langchain": {
45
+ "types": "./dist/adapters/langchain.d.ts",
46
+ "import": "./dist/adapters/langchain.js",
47
+ "default": "./dist/adapters/langchain.js"
48
+ },
49
+ "./anthropic-sdk": {
50
+ "types": "./dist/adapters/anthropic-sdk.d.ts",
51
+ "import": "./dist/adapters/anthropic-sdk.js",
52
+ "default": "./dist/adapters/anthropic-sdk.js"
53
+ },
30
54
  "./package.json": "./package.json"
31
55
  },
32
56
  "bin": {
@@ -59,6 +83,8 @@
59
83
  "example:basic": "BVSALA_DEV_OPEN=1 tsx examples/basic/index.ts",
60
84
  "example:handoff": "BVSALA_DEV_OPEN=1 tsx examples/multi-agent/handoff.ts",
61
85
  "example:serialization": "BVSALA_DEV_OPEN=1 tsx examples/serialization/format-selector.ts",
86
+ "example:otel": "BVSALA_DEV_OPEN=1 tsx examples/exporters/otel.ts",
87
+ "example:focus": "BVSALA_DEV_OPEN=1 tsx examples/exporters/focus.ts",
62
88
  "example:licensed": "tsx examples/licensing/licensed.ts",
63
89
  "license:demo": "tsx examples/licensing/demo.ts",
64
90
  "example:revocation": "tsx examples/licensing/revocation.ts",
@@ -72,6 +98,8 @@
72
98
  "web:dev": "node apps/web/serve.mjs",
73
99
  "prepublishOnly": "pnpm run build",
74
100
  "ab:publish": "tsx benchmarks/publish.ts",
101
+ "ab:sonda": "BVSALA_DEV_OPEN=1 tsx --env-file-if-exists=.env.bench benchmarks/sonda-del-banco.ts",
102
+ "medicion:reproducir": "node apps/web/public/measure/reproduce.mjs",
75
103
  "verja:calibrar": "tsx corpus/calibrar-la-verja.ts",
76
104
  "corpus:arnes": "tsx corpus/arnes/informe.ts",
77
105
  "corpus:auditar": "tsx corpus/auditoria/auditar.ts",
@@ -85,24 +113,62 @@
85
113
  "salida:anatomia": "tsx benchmarks/salida/anatomia.ts",
86
114
  "salida:por-clase": "tsx benchmarks/salida/por-clase.ts",
87
115
  "luz-verde": "bash scripts/luz-verde.sh",
88
- "sonda:sdk": "BVSALA_DEV_OPEN=1 tsx --env-file-if-exists=.env.bench benchmarks/sdk/sonda.ts"
116
+ "sonda:sdk": "BVSALA_DEV_OPEN=1 tsx --env-file-if-exists=.env.bench benchmarks/sdk/sonda.ts",
117
+ "sbom:tarball": "tsx scripts/sbom.ts",
118
+ "cache-semantica": "BVSALA_DEV_OPEN=1 tsx benchmarks/cache-semantica/arnes.ts",
119
+ "cache-semantica:publicar": "tsx benchmarks/cache-semantica/publicar.ts"
89
120
  },
90
121
  "devDependencies": {
91
122
  "@noble/curves": "^2.2.0",
123
+ "@opentelemetry/api": "^1.9.1",
124
+ "@opentelemetry/sdk-trace-base": "^2.11.0",
92
125
  "@types/node": "^22.10.2",
93
126
  "gpt-tokenizer": "^3.4.0",
127
+ "hyparquet": "^1.31.2",
94
128
  "tsup": "^8.3.5",
95
129
  "tsx": "^4.19.2",
96
130
  "typescript": "^5.7.2",
97
- "vitest": "^2.1.8"
131
+ "vitest": "^2.1.8",
132
+ "@ai-sdk/openai": "4.0.78",
133
+ "@anthropic-ai/sdk": "0.128.0",
134
+ "@langchain/core": "1.2.13",
135
+ "@mastra/core": "1.71.0",
136
+ "ai": "7.0.118",
137
+ "ai-v6": "npm:ai@6.0.293",
138
+ "langchain": "1.5.14",
139
+ "zod": "4.6.5"
98
140
  },
99
141
  "packageManager": "pnpm@10.17.1",
100
142
  "peerDependencies": {
101
- "gpt-tokenizer": "^3"
143
+ "@opentelemetry/api": "^1.9.0",
144
+ "gpt-tokenizer": "^3",
145
+ "@anthropic-ai/sdk": ">=0.103.0 <1",
146
+ "@langchain/core": ">=1.1.0 <2",
147
+ "@mastra/core": ">=1.68.0 <2",
148
+ "ai": "^6.0.0 || ^7.0.0",
149
+ "langchain": ">=1.1.0 <2"
102
150
  },
103
151
  "peerDependenciesMeta": {
152
+ "@opentelemetry/api": {
153
+ "optional": true
154
+ },
104
155
  "gpt-tokenizer": {
105
156
  "optional": true
157
+ },
158
+ "@anthropic-ai/sdk": {
159
+ "optional": true
160
+ },
161
+ "@langchain/core": {
162
+ "optional": true
163
+ },
164
+ "@mastra/core": {
165
+ "optional": true
166
+ },
167
+ "ai": {
168
+ "optional": true
169
+ },
170
+ "langchain": {
171
+ "optional": true
106
172
  }
107
173
  }
108
174
  }