@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
|
@@ -8,10 +8,18 @@ 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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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,
|
|
15
23
|
reenvía y avisa.
|
|
16
24
|
|
|
17
25
|
Mide cada petición. Con licencia PRO aplica por defecto **una sola palanca**:
|
|
@@ -21,6 +29,11 @@ por turno) van apagadas hasta que las enciendes. Y mide **todo el tráfico de in
|
|
|
21
29
|
que el terminal enruta por el base URL — no «todo el tráfico»: cada guía dice
|
|
22
30
|
lo que su terminal manda por fuera.
|
|
23
31
|
|
|
32
|
+
En suscripción (login de claude.ai en Claude Code, cuenta ChatGPT en Codex) no
|
|
33
|
+
hay factura por tokens: ese tráfico se mide a precio de lista equivalente y
|
|
34
|
+
nunca se le acredita ahorro ni comisión; la licencia por asiento hace falta
|
|
35
|
+
igual.
|
|
36
|
+
|
|
24
37
|
## Índice
|
|
25
38
|
|
|
26
39
|
| Guía | Base URL exacto | `/v1` | Enrutado [V] | Envoltorio | Estado |
|
|
@@ -34,6 +47,7 @@ lo que su terminal manda por fuera.
|
|
|
34
47
|
| [Droid](droid.md) | `http://127.0.0.1:8402` | sin **[I]** | `~/.factory/settings.json` | — (config de usuario) | validar en mano |
|
|
35
48
|
| [Cline](cline.md) | `http://127.0.0.1:8402/v1` | **con** | UI | — | [V] |
|
|
36
49
|
| [Kilo Code](kilo.md) | `http://127.0.0.1:8402/v1` | con **[I]** | UI | — | validar en mano |
|
|
50
|
+
| [Python y cualquier SDK](../../README.md#python-and-any-other-language-the-local-gateway) (OpenAI · Anthropic) | OpenAI: `http://127.0.0.1:8403/v1` · Anthropic: `http://127.0.0.1:8402` | OpenAI **con**, Anthropic **sin** | `base_url` del SDK | `bvsala gateway --upstream openai` | [V] en test (upstream falso) |
|
|
37
51
|
|
|
38
52
|
Reglas de la columna «Base URL»: **nunca barra final**, **nunca redirecciones**
|
|
39
53
|
(un 3xx en el base URL es fallo por diseño del cliente, y el gateway lo reenvía
|
|
@@ -52,25 +66,43 @@ receta manual de cada guía hace exactamente lo mismo. Para el resto,
|
|
|
52
66
|
|
|
53
67
|
```bash
|
|
54
68
|
npm i -g @bivelio/savings-layer # trae el binario `bvsala`
|
|
55
|
-
export BIVELIO_SERVICE_KEY=bvk_… # clave de servicio, desde tu panel.
|
|
69
|
+
export BIVELIO_SERVICE_KEY=bvk_… # clave de servicio, desde tu panel. Obligatoria: sin ella el gateway no arranca
|
|
56
70
|
bvsala gateway # escucha en http://127.0.0.1:8402
|
|
57
|
-
bvsala gateway --
|
|
71
|
+
bvsala gateway --port 8500 # otro puerto si el 8402 está ocupado
|
|
58
72
|
```
|
|
59
73
|
|
|
60
|
-
Al arrancar imprime
|
|
74
|
+
Al arrancar imprime, entre otras, estas líneas:
|
|
61
75
|
|
|
62
76
|
```
|
|
63
|
-
[bvsala gateway] v0.4.
|
|
64
|
-
[bvsala gateway]
|
|
77
|
+
[bvsala gateway] v0.4.x listening on http://127.0.0.1:8402 → https://api.anthropic.com
|
|
78
|
+
[bvsala gateway] license: PRO, active until 2026-10-24 (+14 days grace), collected with your service key and verified on this machine
|
|
79
|
+
[bvsala gateway] metering: ON (service key present)
|
|
65
80
|
```
|
|
66
81
|
|
|
67
|
-
|
|
68
|
-
|
|
82
|
+
Si no arranca (sale con código 1), el mensaje dice exactamente qué falta:
|
|
83
|
+
|
|
84
|
+
| Mensaje | Qué pasa | Arreglo |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `BIVELIO_SERVICE_KEY is not set` | falta la clave de servicio | créala en el panel (Instalar, paso 2), expórtala y relanza |
|
|
87
|
+
| `BIVELIO_SERVICE_KEY is not recognised` | clave borrada, rotada o copiada a medias | crea otra |
|
|
88
|
+
| `your organization has no active BiVelio license` | no hay suscripción | suscríbete en Cuenta y relanza |
|
|
89
|
+
| `your BiVelio license expired on …` | licencia caducada y sin gracia | renueva en Cuenta |
|
|
90
|
+
| `could not collect your license from savings.bivelio.com` | sin acceso a savings.bivelio.com | revisa la conexión; como respaldo, `BIVELIO_LICENSE_KEY` (se verifica sin red y se sustituye a mano cada período) |
|
|
91
|
+
| un HTTP 402 en el terminal a mitad de sesión | la licencia caducó o se revocó con la sesión abierta | renueva; las peticiones que ya estaban en curso no se cortaron |
|
|
92
|
+
|
|
93
|
+
Si lanzas `bvsala gateway` como servicio (launchd/systemd), un fallo de licencia
|
|
94
|
+
queda en el log del servicio con código 1 y el terminal solo verá «connection
|
|
95
|
+
refused»: mira ese log.
|
|
69
96
|
|
|
70
97
|
Un gateway habla con **un** upstream (`BVSALA_GATEWAY_UPSTREAM`, por defecto
|
|
71
98
|
`https://api.anthropic.com`). Los terminales del dialecto Anthropic comparten
|
|
72
99
|
instancia; Codex necesita la suya contra OpenAI — `bvsala codex` la arranca
|
|
73
|
-
sola con el upstream correcto según tu modo de auth (ver su guía).
|
|
100
|
+
sola con el upstream correcto según tu modo de auth (ver su guía). Desde la
|
|
101
|
+
próxima versión, `bvsala gateway --upstream openai|anthropic|<origen>` lo elige
|
|
102
|
+
sin variable de entorno (el flag manda sobre `BVSALA_GATEWAY_UPSTREAM`). Para un
|
|
103
|
+
SDK de OpenAI (Python, Node…) arranca uno con `--upstream openai` en otro puerto;
|
|
104
|
+
en streaming, la petición tiene que llevar `stream_options.include_usage` o el
|
|
105
|
+
gateway la reenvía sin medirla y lo dice en el log.
|
|
74
106
|
|
|
75
107
|
### 2 · Comprobar que mide: el log del gateway
|
|
76
108
|
|
|
@@ -88,13 +120,56 @@ las otras líneas:
|
|
|
88
120
|
|---|---|---|
|
|
89
121
|
| `[gateway] POST /v1/v1/messages → 404` | el base URL lleva `/v1` **de más** para ese terminal | quita el `/v1` |
|
|
90
122
|
| `[gateway] POST /messages → 404` | al base URL le **falta** `/v1` (el SDK solo añade `/messages`) | añade el `/v1` |
|
|
91
|
-
| `[
|
|
123
|
+
| `[gateway] POST /v1/messages → 402 (license expired)` | la licencia caducó, se revocó o la org está bloqueada: las peticiones NUEVAS se rechazan (las que estaban en curso terminaron) | renueva en Cuenta; el terminal enseña el motivo |
|
|
92
124
|
| nada de nada | el terminal no está apuntando al gateway (config no leída, env pisado por config, otro puerto) | revisa la trampa de su guía |
|
|
93
125
|
|
|
126
|
+
Cada petición que el gateway reenvía **sin medir** deja su propia línea con
|
|
127
|
+
`· not metered: <motivo>`, y en su lugar no se estima nada. Los motivos, tal y
|
|
128
|
+
como salen hoy:
|
|
129
|
+
|
|
130
|
+
| `· not metered: …` | Qué pasa | Arreglo |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `no BIVELIO_SERVICE_KEY` | un gateway arrancado desde código sin clave de servicio: reenvía, no mide (el CLI `bvsala` ni arranca sin ella) | exporta `BIVELIO_SERVICE_KEY` |
|
|
133
|
+
| `model "…" has no list price in the catalog` | modelo fuera del catálogo de precios, a propósito: un precio presente y equivocado es peor que ninguno (la consola lo avisa una vez por modelo) | nada; se reenvía sin medir |
|
|
134
|
+
| `could not read the model from the request body (content-encoding: …)` | el cuerpo no se pudo leer: JSON ilegible o una compresión que este Node no abre (zstd, el de Codex, necesita Node ≥ 22.15) | actualiza Node |
|
|
135
|
+
| `the request did not ask for usage (stream_options.include_usage)` | Chat Completions en stream sin `include_usage`: el proveedor no manda contador | añade `stream_options={"include_usage": True}` a la llamada, o arranca con `BVSALA_GATEWAY_USAGE_INJECT=1` |
|
|
136
|
+
| `…; usage injection skips compressed request bodies` | la inyección está puesta, pero solo añade la opción a cuerpos sin comprimir | pídelo en la llamada |
|
|
137
|
+
| `…, and the upstream rejects that option` | el upstream rechazó `stream_options` y la inyección quedó apagada para el resto del proceso | en stream no hay arreglo desde el gateway; con `stream: false` sí se mide |
|
|
138
|
+
| `the stream ended without the provider's final usage event` | el stream se cortó antes de su evento final (`message_delta`, `response.completed`/`incomplete`/`failed`, el chunk de uso o `[DONE]`): sin él no hay contador | nada |
|
|
139
|
+
| `the response ended as response.failed without usage` | la Responses API cerró con `response.failed` sin `usage`. Si lo trae, sí se mide | nada |
|
|
140
|
+
| `the response ended as response.incomplete without usage` | la Responses API cerró con `response.incomplete` sin `usage`. Si lo trae, sí se mide: un `response.incomplete` por `max_output_tokens` o por el filtro de contenido lo factura el proveedor | nada |
|
|
141
|
+
| `the response carried no usage` | respuesta JSON (`stream: false`) sin `usage` | nada |
|
|
142
|
+
| `the response was too large to read its usage` | respuesta JSON de más de 5 MB: el gateway no la guarda entera para leerla | nada |
|
|
143
|
+
| `the response is neither JSON nor an event stream (content-type: …)` | el upstream contestó con otro tipo de contenido en una ruta que sí sabe medir | revisa el upstream |
|
|
144
|
+
| `no usage dialect for this route` | ruta sin contador que el gateway sepa leer (embeddings, imágenes, audio…): solo mide `/v1/messages`, `…/chat/completions` y `…/responses` | nada |
|
|
145
|
+
|
|
146
|
+
Con `BVSALA_GATEWAY_COALESCE=1` (apagada por defecto), una petición idéntica a
|
|
147
|
+
otra que sigue en vuelo no viaja: recibe la réplica y su línea lleva
|
|
148
|
+
`· COALESCED (call avoided)`. Detrás va `· by reference: floor $… (cache read
|
|
149
|
+
price, …)` cuando se valora con el contador de la petición que sí viajó —a
|
|
150
|
+
precio de lectura de caché, sin primas y con la salida solo si el cuerpo trae
|
|
151
|
+
`temperature: 0` y el modelo no razona (la misma guarda de modelo que el SDK)—,
|
|
152
|
+
o `· estimated, not by reference: <motivo>` cuando no. Esa línea es la
|
|
153
|
+
valoración local: el panel todavía recibe estas peticiones como `estimated`, y
|
|
154
|
+
no entran en la base de la comisión, porque el gateway aún no lee los tokens de
|
|
155
|
+
razonamiento que declara el proveedor y la regla `reference-v2` los exige.
|
|
156
|
+
|
|
157
|
+
| `· estimated, not by reference: …` | Por qué no |
|
|
158
|
+
|---|---|
|
|
159
|
+
| `no-primary-record` | la petición que viajó no dejó su contador final (ni evento final del stream ni `usage` en el JSON) |
|
|
160
|
+
| `subscription` | tráfico de suscripción (Claude o ChatGPT): no hay factura por token |
|
|
161
|
+
| `unofficial-upstream` | el gateway no habla con `api.anthropic.com` ni `api.openai.com` (un mock, un proxy, Azure, un revendedor): el precio de lista no está en ninguna factura |
|
|
162
|
+
| `layer-changed-request` | una palanca cambió lo que viajó (modelo, cuerpo, cabeceras; también la rebaja de TTL de la caché): ya no es la petición del cliente byte a byte |
|
|
163
|
+
| `primary-failed` | la petición que viajó cerró con `response.failed` |
|
|
164
|
+
| `client-disconnected` | el cliente de esta petición se fue antes de recibir la réplica entera |
|
|
165
|
+
| `service-tier` | el `service_tier` pedido o el servido no tiene precio por token publicado (`scale`, uno nuevo) |
|
|
166
|
+
| `no-cached-price` | el modelo no tiene precio de lectura de caché en su propia fila del catálogo |
|
|
167
|
+
| `no-credit` | el contador no deja nada que acreditar |
|
|
168
|
+
|
|
94
169
|
Claude Code manda además un `HEAD /api/hello` antes de la primera llamada: es
|
|
95
170
|
la sonda de vida, no una inferencia.
|
|
96
171
|
|
|
97
|
-
`bvsala doctor [--
|
|
172
|
+
`bvsala doctor [--port N]`: sondea el PATH y la existencia de las configs de
|
|
98
173
|
usuario (nunca lee su contenido) e imprime el bloque exacto por terminal con el
|
|
99
174
|
puerto indicado. Solo imprime: no escribe ninguna configuración.
|
|
100
175
|
|
|
@@ -134,3 +209,107 @@ no se promociona ni se documenta como receta.
|
|
|
134
209
|
di eso, nunca «aprobado por Anthropic». El contrato del gateway es una lista
|
|
135
210
|
abierta de cabeceras y campos que crece con cada release del terminal; el
|
|
136
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.
|
|
@@ -12,8 +12,10 @@ construcción, con test.
|
|
|
12
12
|
`apiKeyHelper`): es el segmento que BV-SALA mide y factura. Con login de
|
|
13
13
|
claude.ai el enrutado también funciona — la doc oficial lo sanciona: el login
|
|
14
14
|
sigue siendo la credencial activa y «its usage limits and billing apply» [V] —
|
|
15
|
-
pero ahí no hay factura por token
|
|
16
|
-
|
|
15
|
+
pero ahí no hay factura por token: el gateway lo mide a precio de lista
|
|
16
|
+
equivalente y nunca le acredita ahorro ni comisión (la licencia por asiento
|
|
17
|
+
hace falta igual).
|
|
18
|
+
- `bvsala` instalado, `BIVELIO_SERVICE_KEY` exportada y una licencia BiVelio activa (la recoge el gateway con la clave; ver [README](README.md)).
|
|
17
19
|
|
|
18
20
|
## Envoltorio: `bvsala claude` (recomendado)
|
|
19
21
|
|
|
@@ -115,8 +117,13 @@ con cada release de Claude Code; BV-SALA repite el smoke test de `bvsala claude`
|
|
|
115
117
|
contra cada versión nueva.
|
|
116
118
|
|
|
117
119
|
Las cabeceras `x-claude-code-session-id` / `agent-id` / `parent-agent-id`
|
|
118
|
-
llegan en el tráfico real y
|
|
119
|
-
|
|
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.
|
|
120
127
|
|
|
121
128
|
## Nota BYOK y wording
|
|
122
129
|
|
package/docs/terminales/cline.md
CHANGED
|
@@ -14,7 +14,7 @@ que se usa.
|
|
|
14
14
|
|
|
15
15
|
- Cline instalado en VS Code.
|
|
16
16
|
- Tu **API key** de Anthropic.
|
|
17
|
-
- `bvsala gateway` corriendo en `http://127.0.0.1:8402` (ver [README](README.md)).
|
|
17
|
+
- `bvsala gateway` corriendo en `http://127.0.0.1:8402` (necesita `BIVELIO_SERVICE_KEY` y una licencia BiVelio activa; ver [README](README.md)).
|
|
18
18
|
|
|
19
19
|
## Pasos en la UI
|
|
20
20
|
|
package/docs/terminales/codex.md
CHANGED
|
@@ -11,9 +11,10 @@ cifras.
|
|
|
11
11
|
|
|
12
12
|
- `codex` en el PATH.
|
|
13
13
|
- Login en Codex: **API key de OpenAI** (el caso que se monetiza) o
|
|
14
|
-
suscripción ChatGPT (se enruta y se mide
|
|
15
|
-
factura por tokens;
|
|
16
|
-
|
|
14
|
+
suscripción ChatGPT (se enruta y se mide a precio de lista equivalente, **sin
|
|
15
|
+
comisión**: no hay factura por tokens; la licencia por asiento hace falta
|
|
16
|
+
igual; ver más abajo).
|
|
17
|
+
- `bvsala` instalado, `BIVELIO_SERVICE_KEY` exportada y una licencia BiVelio activa (la recoge el gateway con la clave; ver [README](README.md)).
|
|
17
18
|
|
|
18
19
|
## Envoltorio: `bvsala codex [args…]`
|
|
19
20
|
|
|
@@ -92,26 +93,26 @@ que el tráfico real enseñó y el gateway ya contempla:
|
|
|
92
93
|
de Codex frente a su backend: aparece igual **sin** gateway (comprobado).
|
|
93
94
|
4. **Barra final.** `…/v1/` no.
|
|
94
95
|
|
|
95
|
-
## Modo suscripción ChatGPT — validado, medido,
|
|
96
|
+
## Modo suscripción ChatGPT — validado, medido, sin comisión
|
|
96
97
|
|
|
97
98
|
Validado en mano el 30-08 (prefijo `/backend-api/codex`, upstream
|
|
98
99
|
`https://chatgpt.com`): 200, stream íntegro, `usage` presente en
|
|
99
100
|
`response.completed` y registro `measured` en la ingesta. **Pero un suscriptor
|
|
100
101
|
no paga por tokens**: la cifra que ve es el equivalente a precio de lista por
|
|
101
102
|
API key, no una factura; y como la comisión de BV-SALA solo toca ahorro medido
|
|
102
|
-
sobre gasto real, este tráfico **no
|
|
103
|
+
sobre gasto real, este tráfico **no genera comisión** — igual que el OAuth de
|
|
103
104
|
suscripción en Claude Code (`docs/PASARELA-F0.md`: «el caso que NO
|
|
104
105
|
facturamos»). OpenAI ni bendice ni prohíbe el proxy de suscripción (silencio
|
|
105
106
|
del staff, a diferencia de Anthropic, que lo documenta). Postura idéntica a
|
|
106
107
|
Claude Code: cero persistencia de `Authorization` / `ChatGPT-Account-ID`,
|
|
107
108
|
binario sin modificar, y no se promociona como modo de ahorro.
|
|
108
109
|
|
|
109
|
-
**
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
**Comisión 0 por diseño (desde la versión en que el gateway exige licencia):**
|
|
111
|
+
el gateway clasifica como suscripción el tráfico de `/backend-api/…` o con
|
|
112
|
+
upstream chatgpt.com (y el de Claude Code con `anthropic-beta: oauth-*`) y no le
|
|
113
|
+
acredita ahorro: `baseline == actual`. Laguna que queda: el registro no lleva
|
|
114
|
+
etiqueta propia en el wire, así que el panel no distingue suscripción de API key
|
|
115
|
+
(exige migración; fuera de este frente).
|
|
115
116
|
|
|
116
117
|
## Validaciones en mano
|
|
117
118
|
|
package/docs/terminales/crush.md
CHANGED
|
@@ -10,7 +10,7 @@ relativa, y eso dicta la forma del URL: **con `/v1`, sin barra final**.
|
|
|
10
10
|
- `crush` instalado.
|
|
11
11
|
- `ANTHROPIC_API_KEY` exportada (Crush retiró su OAuth de Claude en ene-2026:
|
|
12
12
|
solo API key).
|
|
13
|
-
- `bvsala` instalado
|
|
13
|
+
- `bvsala` instalado, `BIVELIO_SERVICE_KEY` exportada y una licencia BiVelio activa (la recoge el gateway con la clave; ver [README](README.md)).
|
|
14
14
|
|
|
15
15
|
## Config de usuario: `~/.config/crush/crushrc`
|
|
16
16
|
|
package/docs/terminales/droid.md
CHANGED
|
@@ -9,7 +9,7 @@ lo dice tal cual; no hay validación hecha.
|
|
|
9
9
|
## Qué necesitas
|
|
10
10
|
|
|
11
11
|
- `droid` instalado, en modo BYOK con tu **API key** de Anthropic.
|
|
12
|
-
- `bvsala` instalado
|
|
12
|
+
- `bvsala` instalado, `BIVELIO_SERVICE_KEY` exportada y una licencia BiVelio activa (la recoge el gateway con la clave; ver [README](README.md)).
|
|
13
13
|
|
|
14
14
|
## Config de usuario: `~/.factory/settings.json`
|
|
15
15
|
|
package/docs/terminales/goose.md
CHANGED
|
@@ -9,7 +9,7 @@ forma del URL.
|
|
|
9
9
|
|
|
10
10
|
- `goose` en el PATH, con el proveedor Anthropic elegido.
|
|
11
11
|
- `ANTHROPIC_API_KEY` exportada (Goose es solo BYOK).
|
|
12
|
-
- `bvsala` instalado
|
|
12
|
+
- `bvsala` instalado, `BIVELIO_SERVICE_KEY` exportada y una licencia BiVelio activa (la recoge el gateway con la clave; ver [README](README.md)).
|
|
13
13
|
|
|
14
14
|
## Envoltorio: `bvsala goose`
|
|
15
15
|
|
package/docs/terminales/kilo.md
CHANGED
|
@@ -10,7 +10,7 @@ gateway.** No hay validación hecha; esta guía lo dice tal cual.
|
|
|
10
10
|
|
|
11
11
|
- Kilo Code instalado en VS Code.
|
|
12
12
|
- Tu **API key** de Anthropic.
|
|
13
|
-
- `bvsala gateway` corriendo en `http://127.0.0.1:8402` (ver [README](README.md)).
|
|
13
|
+
- `bvsala gateway` corriendo en `http://127.0.0.1:8402` (necesita `BIVELIO_SERVICE_KEY` y una licencia BiVelio activa; ver [README](README.md)).
|
|
14
14
|
|
|
15
15
|
## Pasos en la UI
|
|
16
16
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
## Qué necesitas
|
|
8
8
|
|
|
9
9
|
- `opencode` instalado, proveedor `anthropic` con tu **API key** de Anthropic.
|
|
10
|
-
- `bvsala` instalado
|
|
10
|
+
- `bvsala` instalado, `BIVELIO_SERVICE_KEY` exportada y una licencia BiVelio activa (la recoge el gateway con la clave; ver [README](README.md)).
|
|
11
11
|
|
|
12
12
|
## Config de usuario: `~/.config/opencode/opencode.json`
|
|
13
13
|
|
|
@@ -10,7 +10,7 @@ catálogo a propósito (más abajo).
|
|
|
10
10
|
- `qwen` en el PATH.
|
|
11
11
|
- **Una API key de Anthropic** (`ANTHROPIC_API_KEY`), con Qwen Code configurado
|
|
12
12
|
para usar el proveedor Anthropic.
|
|
13
|
-
- `bvsala` instalado
|
|
13
|
+
- `bvsala` instalado, `BIVELIO_SERVICE_KEY` exportada y una licencia BiVelio activa (la recoge el gateway con la clave; ver [README](README.md)).
|
|
14
14
|
|
|
15
15
|
## Config de usuario: `~/.qwen/settings.json`
|
|
16
16
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bivelio/savings-layer",
|
|
3
|
-
"version": "0.4.
|
|
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
|
-
"
|
|
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
|
}
|