@bivelio/savings-layer 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,6 +33,14 @@ charge a flat subscription, so there is no per‑token bill to cut. BV‑SALA is
33
33
  applications that call the API with their own key. The whole story, in plain words:
34
34
  **[Where does the saving install?](https://savings.bivelio.com/en/how-it-works)**
35
35
 
36
+ **Claude Code, Codex CLI, Goose or Qwen Code with an API key?** The package also
37
+ ships a local measuring gateway: `bvsala claude` (or `bvsala codex`, `bvsala
38
+ goose`, `bvsala qwen`) launches the terminal through it and your real spend
39
+ shows up in your dashboard, token by token — nothing to configure, nothing
40
+ persisted. For opencode, Crush, Droid, Cline or Kilo Code, `bvsala doctor`
41
+ prints the exact config block for each one you have installed. Today it
42
+ measures; it does not optimize yet.
43
+
36
44
  ## Try it before you install
37
45
 
38
46
  Run the real optimizer **free in your browser** — pick an example or paste your own
@@ -60,7 +68,7 @@ export BIVELIO_SERVICE_KEY=… # service-account key: auto-renewal + seat mete
60
68
  ```ts
61
69
  import { createSavingsLayer, OpenAICompatibleProvider } from "@bivelio/savings-layer";
62
70
 
63
- // Your provider. LLM traffic goes straight to it — BV-SALA never proxies it.
71
+ // Your provider. LLM traffic goes straight to it — the SDK never proxies it.
64
72
  // Any OpenAI-compatible endpoint works: OpenAI, Azure, vLLM, SGLang, a gateway.
65
73
  const provider = new OpenAICompatibleProvider({
66
74
  baseUrl: "https://api.openai.com/v1",
@@ -1,21 +1,171 @@
1
1
  #!/usr/bin/env node
2
- import {spawn}from'child_process';import {createServer}from'http';import {randomUUID,createHash}from'crypto';var te=new Set;function v(t,e){te.has(t)||(te.add(t),console.warn(e));}var w=class{options;now;flushIntervalMs;maxBatch;maxBuffer;buffer=[];seatId;ci;timer;inflight=null;fallosSeguidos=0;constructor(e){this.options=e,this.now=e.now??(()=>Date.now()),this.flushIntervalMs=e.flushIntervalMs??15e3,this.maxBatch=e.maxBatch??50,this.maxBuffer=e.maxBuffer??1e3,this.ci=e.ci??false;}setSeat(e,n){e&&(this.seatId=e),this.ci=n;}record(e){this.buffer.push({report:e,at:this.now()}),this.buffer.length>this.maxBuffer&&this.buffer.splice(0,this.buffer.length-this.maxBuffer),this.buffer.length>=this.maxBatch&&this.flush();}start(){this.timer||(this.timer=setInterval(()=>{this.flush();},this.flushIntervalMs),this.timer.unref?.());}stop(){this.timer&&(clearInterval(this.timer),this.timer=void 0),this.flush();}async stopAndDrain(e){this.timer&&(clearInterval(this.timer),this.timer=void 0);let n=e??Math.ceil(this.maxBuffer/this.maxBatch)+4,r=0;for(let o=0;o<n&&this.buffer.length>0;o++){let s=this.buffer.length;if(await this.flush(),r=this.buffer.length>=s?r+1:0,r>=2)break}}async flush(){if(this.inflight)return this.inflight;if(this.buffer.length!==0){this.inflight=this.hacerFlush();try{await this.inflight;}finally{this.inflight=null;}}}async hacerFlush(){let e=this.options.fetchImpl??globalThis.fetch;if(!e)return;let n=this.buffer.splice(0,this.maxBatch);try{let r=n.map(s=>this.toWire(s)),o=await e(this.options.url,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${this.options.serviceAccountKey}`},body:JSON.stringify(r)});o.ok?this.fallosSeguidos=0:o.status>=500?(this.fallosSeguidos++,this.requeue(n),this.avisarDeFallo(`el servidor respondi\xF3 ${o.status}`)):o.status===401||o.status===403?(v(`metering-auth-${o.status}`,`[bv-sala] el env\xEDo de uso est\xE1 siendo rechazado con HTTP ${o.status} en ${this.options.url}. Revisa 'serviceAccountKey' (\xBFrotada? \xBFrevocada? \xBFde otra organizaci\xF3n?). Mientras no funcione NO se reporta uso: el panel dir\xE1 que no ve tr\xE1fico y los asientos facturables saldr\xE1n a cero.`),this.options.onAuthError?.(o.status)):(this.fallosSeguidos++,this.avisarDeFallo(o.status===404||o.status===405?`HTTP ${o.status}: la URL de env\xEDo parece equivocada`:`el servidor rechaz\xF3 el lote con HTTP ${o.status}`));}catch(r){this.fallosSeguidos++,this.requeue(n),this.avisarDeFallo(r instanceof Error?r.message:"error de red");}}avisarDeFallo(e){if(this.options.silenceWarnings)return;let n=this.fallosSeguidos;n<3||(n-3)%12!==0||console.warn(`[bv-sala] el env\xEDo de uso lleva ${n} intentos seguidos fallando contra ${this.options.url} (${e}). Las peticiones SIGUEN funcionando, pero el uso no est\xE1 llegando: el panel dir\xE1 que no ve tr\xE1fico y los asientos facturables saldr\xE1n a cero. Comprueba 'meteringUrl' \u2014 por defecto es \`\${licenseServerUrl}/api/metering/usage\`.`);}requeue(e){this.buffer=[...e,...this.buffer],this.buffer.length>this.maxBuffer&&(this.buffer.length=this.maxBuffer);}toWire(e){let n=e.report;return {orgId:this.options.orgId??"self",...this.seatId?{seatId:this.seatId}:{},...this.ci?{ci:true}:{},requestId:n.requestId,model:n.model,at:e.at,avoidedLlm:n.calls.avoidedLlm,actualLlm:n.calls.actualLlm,baselineInputTokens:n.tokens.baselineInput,actualInputTokens:n.tokens.actualInput,eliminatedInputTokens:n.tokens.eliminatedInput,compressedInputTokens:n.tokens.compressedInput,baselineOutputTokens:n.tokens.baselineOutput,actualOutputTokens:n.tokens.actualOutput,providerCachedInputTokens:n.tokens.providerCachedInput??0,cacheResult:n.cache.result,baselineCost:n.cost.baselineEstimate,actualCost:n.cost.actual,netSavings:n.cost.netSavings,...n.cost.basis?{basis:n.cost.basis}:{}}}};function O(){return globalThis.process}function be(t){if(/^[0-9a-f]{12}$/.test(t)||/^\d{1,3}(\.\d{1,3}){3}$/.test(t)||/^ip-\d{1,3}(-\d{1,3}){3}$/i.test(t))return true;let e="[bcdfghjklmnpqrstvwxz2456789]";return new RegExp(`-${e}{8,10}-${e}{5}$`).test(t)}function ve(t){return !!(t.AWS_LAMBDA_FUNCTION_NAME??t.LAMBDA_TASK_ROOT??t.VERCEL??t.VERCEL_ENV??t.K_SERVICE??t.FUNCTION_TARGET??t.FUNCTIONS_WORKER_RUNTIME??t.NETLIFY)}async function ye(){if(O()?.execPath)try{let e=await import(["node","os"].join(":")),n=typeof e.hostname=="function"?e.hostname():"";return n.length>0?n:void 0}catch{return}}async function ke(t,e=ye){if(t.KUBERNETES_SERVICE_HOST||ve(t))return;let n=t.HOSTNAME??t.COMPUTERNAME??await e();if(!(!n||be(n)))return n}async function xe(){let t=O();if(!t||!t.env)return;let e=t.env,n=await ke(e),r=[t.platform,t.arch,t.execPath,n,e.USER??e.USERNAME??e.LOGNAME,e.HOME??e.USERPROFILE].filter(o=>typeof o=="string"&&o.length>0);return r.length?r.join("|"):void 0}var we=["CI","CONTINUOUS_INTEGRATION","GITHUB_ACTIONS","GITLAB_CI","CIRCLECI","TRAVIS","JENKINS_URL","BUILDKITE","TF_BUILD","TEAMCITY_VERSION","BITBUCKET_BUILD_NUMBER","CODEBUILD_BUILD_ID","HEROKU_TEST_RUN_ID","DRONE"];function D(){let t=O()?.env;if(!t)return false;for(let e of we){let n=t[e];if(n&&n!=="false"&&n!=="0")return true}return false}async function Se(t,e){let n=globalThis.crypto,r=new TextEncoder;if(n?.subtle)try{let i=await n.subtle.importKey("raw",r.encode(t),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),l=await n.subtle.sign("HMAC",i,r.encode(e));return [...new Uint8Array(l)].map(u=>u.toString(16).padStart(2,"0")).join("")}catch{}let o=2166136261,s=`${t}|${e}`;for(let i=0;i<s.length;i++)o^=s.charCodeAt(i),o=Math.imul(o,16777619);return (o>>>0).toString(16).padStart(8,"0")}async function A(t){let n=O()?.env?.BVSALA_SEAT_ID;if(n&&Te.test(n))return n;let r=await xe();if(!r)return;let o=await Se(t,r);if(o)return `m-${o.slice(0,32)}`}var Te=/^[A-Za-z0-9._:/-]{1,128}$/;var F={"gpt-4.1":{inputPer1k:.002,outputPer1k:.008,cachedInputPer1k:5e-4},"gpt-4.1-mini":{inputPer1k:4e-4,outputPer1k:.0016,cachedInputPer1k:1e-4},"gpt-4.1-nano":{inputPer1k:1e-4,outputPer1k:4e-4,cachedInputPer1k:25e-6},"gpt-4o":{inputPer1k:.0025,outputPer1k:.01,cachedInputPer1k:.00125},"gpt-4o-mini":{inputPer1k:15e-5,outputPer1k:6e-4,cachedInputPer1k:75e-6},"gpt-4-turbo":{inputPer1k:.01,outputPer1k:.03},"gpt-4":{inputPer1k:.03,outputPer1k:.06},"gpt-3.5-turbo":{inputPer1k:5e-4,outputPer1k:.0015},"gpt-5":{inputPer1k:.00125,outputPer1k:.01,cachedInputPer1k:125e-6},"gpt-5-codex":{inputPer1k:.00125,outputPer1k:.01,cachedInputPer1k:125e-6},"gpt-5-mini":{inputPer1k:25e-5,outputPer1k:.002,cachedInputPer1k:25e-6},"gpt-5-nano":{inputPer1k:5e-5,outputPer1k:4e-4,cachedInputPer1k:5e-6},"gpt-5.1-codex":{inputPer1k:.00125,outputPer1k:.01,cachedInputPer1k:125e-6},"gpt-5.1-codex-max":{inputPer1k:.00125,outputPer1k:.01,cachedInputPer1k:125e-6},"gpt-5.1-codex-mini":{inputPer1k:25e-5,outputPer1k:.002,cachedInputPer1k:25e-6},"gpt-5.1-thinking":{inputPer1k:.00125,outputPer1k:.01,cachedInputPer1k:125e-6},"gpt-5.2":{inputPer1k:.00175,outputPer1k:.014,cachedInputPer1k:175e-6},"gpt-5.2-codex":{inputPer1k:.00175,outputPer1k:.014,cachedInputPer1k:175e-6},"gpt-5.3-codex":{inputPer1k:.00175,outputPer1k:.014,cachedInputPer1k:175e-6},"gpt-5.4":{inputPer1k:.0025,outputPer1k:.015,cachedInputPer1k:25e-5},"gpt-5.4-mini":{inputPer1k:75e-5,outputPer1k:.0045,cachedInputPer1k:75e-6},"gpt-5.4-nano":{inputPer1k:2e-4,outputPer1k:.00125,cachedInputPer1k:2e-5},"gpt-5.5":{inputPer1k:.005,outputPer1k:.03,cachedInputPer1k:5e-4},"gpt-5.6-luna":{inputPer1k:2e-4,outputPer1k:.0012,cachedInputPer1k:2e-5},"gpt-5.6-sol":{inputPer1k:.004,outputPer1k:.02,cachedInputPer1k:4e-4},"gpt-5.6-terra":{inputPer1k:.002,outputPer1k:.012,cachedInputPer1k:2e-4},"claude-opus-4-5":{inputPer1k:.005,outputPer1k:.025,cachedInputPer1k:5e-4},"claude-opus-4-6":{inputPer1k:.005,outputPer1k:.025,cachedInputPer1k:5e-4},"claude-opus-4-7":{inputPer1k:.005,outputPer1k:.025,cachedInputPer1k:5e-4},"claude-opus-4-8":{inputPer1k:.005,outputPer1k:.025,cachedInputPer1k:5e-4},"claude-opus-5":{inputPer1k:.005,outputPer1k:.025,cachedInputPer1k:5e-4},"claude-opus-4":{inputPer1k:.015,outputPer1k:.075,cachedInputPer1k:.0015},"claude-sonnet-4-5":{inputPer1k:.003,outputPer1k:.015,cachedInputPer1k:3e-4},"claude-sonnet-4-6":{inputPer1k:.003,outputPer1k:.015,cachedInputPer1k:3e-4},"claude-sonnet-5":{inputPer1k:.002,outputPer1k:.01,cachedInputPer1k:2e-4},"claude-fable-5":{inputPer1k:.01,outputPer1k:.05,cachedInputPer1k:.001},"claude-sonnet-4":{inputPer1k:.003,outputPer1k:.015,cachedInputPer1k:3e-4},"claude-haiku-4-5":{inputPer1k:.001,outputPer1k:.005,cachedInputPer1k:1e-4},"claude-3-5-sonnet":{inputPer1k:.003,outputPer1k:.015,cachedInputPer1k:3e-4},"claude-3-5-haiku":{inputPer1k:8e-4,outputPer1k:.004,cachedInputPer1k:8e-5},"claude-3-opus":{inputPer1k:.015,outputPer1k:.075,cachedInputPer1k:.0015},"claude-3-haiku":{inputPer1k:25e-5,outputPer1k:.00125,cachedInputPer1k:3e-5},"gemini-3.7-flash":{inputPer1k:75e-5,outputPer1k:.00375,cachedInputPer1k:75e-6},"gemini-3.5-flash":{inputPer1k:.0015,outputPer1k:.009,cachedInputPer1k:15e-5},"gemini-3.5-flash-lite":{inputPer1k:3e-4,outputPer1k:.0025,cachedInputPer1k:3e-5},"gemini-3.1-pro-preview":{inputPer1k:.002,outputPer1k:.012,cachedInputPer1k:2e-4},"gemini-2.5-pro":{inputPer1k:.00125,outputPer1k:.01,cachedInputPer1k:125e-6},"gemini-2.5-flash":{inputPer1k:3e-4,outputPer1k:.0025,cachedInputPer1k:3e-5}};function ne(t,e,n){if(!n)return;let r=t[n];if(r)return r;let o=n.lastIndexOf("/"),s=o>=0?n.slice(o+1):n,i=t[s];if(i)return i;for(let l of e)if(s.startsWith(l)){let u=s.charAt(l.length);if(u===""||u==="-"||u==="."||u==="_")return t[l]}}function V(t){return Object.keys(t).sort((e,n)=>n.length-e.length)}var Ce=V(F);function E(t){return ne(F,Ce,t)}var I=class{resto="";uso=null;eventos={};observar(e){this.resto+=typeof e=="string"?e:e.toString("utf8");let n;for(;(n=this.resto.indexOf(`
2
+ import {spawn}from'child_process';import {existsSync,readFileSync}from'fs';import {homedir}from'os';import {join,delimiter}from'path';import {createServer}from'http';import {randomUUID,createHash}from'crypto';import*as T from'zlib';var be=new Set;function S(e,t){be.has(e)||(be.add(e),console.warn(t));}var E=class{options;now;flushIntervalMs;maxBatch;maxBuffer;buffer=[];seatId;ci;timer;inflight=null;fallosSeguidos=0;constructor(t){this.options=t,this.now=t.now??(()=>Date.now()),this.flushIntervalMs=
3
+ t.flushIntervalMs??15e3,this.maxBatch=t.maxBatch??50,this.maxBuffer=t.maxBuffer??1e3,this.ci=t.ci??false;}setSeat(t,n){t&&(this.seatId=t),this.ci=n;}record(t){this.buffer.push({report:t,at:this.now()}),this.
4
+ buffer.length>this.maxBuffer&&this.buffer.splice(0,this.buffer.length-this.maxBuffer),this.buffer.length>=this.maxBatch&&this.flush();}start(){this.timer||(this.timer=setInterval(()=>{this.flush();},this.
5
+ flushIntervalMs),this.timer.unref?.());}stop(){this.timer&&(clearInterval(this.timer),this.timer=void 0),this.flush();}async stopAndDrain(t){this.timer&&(clearInterval(this.timer),this.timer=void 0);let n=t??
6
+ Math.ceil(this.maxBuffer/this.maxBatch)+4,r=0;for(let o=0;o<n&&this.buffer.length>0;o++){let s=this.buffer.length;if(await this.flush(),r=this.buffer.length>=s?r+1:0,r>=2)break}}async flush(){if(this.
7
+ inflight)return this.inflight;if(this.buffer.length!==0){this.inflight=this.hacerFlush();try{await this.inflight;}finally{this.inflight=null;}}}async hacerFlush(){let t=this.options.fetchImpl??globalThis.
8
+ fetch;if(!t)return;let n=this.buffer.splice(0,this.maxBatch);try{let r=n.map(s=>this.toWire(s)),o=await t(this.options.url,{method:"POST",headers:{"content-type":"application/json",authorization:`Bear\
9
+ er ${this.options.serviceAccountKey}`},body:JSON.stringify(r)});o.ok?this.fallosSeguidos=0:o.status>=500?(this.fallosSeguidos++,this.requeue(n),this.avisarDeFallo(`el servidor respondi\xF3 ${o.status}`)):
10
+ o.status===401||o.status===403?(S(`metering-auth-${o.status}`,`[bv-sala] el env\xEDo de uso est\xE1 siendo rechazado con HTTP ${o.status} en ${this.options.url}. Revisa 'serviceAccountKey' (\xBFrotada? \xBF\
11
+ revocada? \xBFde otra organizaci\xF3n?). Mientras no funcione NO se reporta uso: el panel dir\xE1 que no ve tr\xE1fico y los asientos facturables saldr\xE1n a cero.`),this.options.onAuthError?.(o.status)):
12
+ (this.fallosSeguidos++,this.avisarDeFallo(o.status===404||o.status===405?`HTTP ${o.status}: la URL de env\xEDo parece equivocada`:`el servidor rechaz\xF3 el lote con HTTP ${o.status}`));}catch(r){this.
13
+ fallosSeguidos++,this.requeue(n),this.avisarDeFallo(r instanceof Error?r.message:"error de red");}}avisarDeFallo(t){if(this.options.silenceWarnings)return;let n=this.fallosSeguidos;n<3||(n-3)%12!==0||console.
14
+ warn(`[bv-sala] el env\xEDo de uso lleva ${n} intentos seguidos fallando contra ${this.options.url} (${t}). Las peticiones SIGUEN funcionando, pero el uso no est\xE1 llegando: el panel dir\xE1 que no ve tr\xE1\
15
+ fico y los asientos facturables saldr\xE1n a cero. Comprueba 'meteringUrl' \u2014 por defecto es \`\${licenseServerUrl}/api/metering/usage\`.`);}requeue(t){this.buffer=[...t,...this.buffer],this.buffer.
16
+ length>this.maxBuffer&&(this.buffer.length=this.maxBuffer);}toWire(t){let n=t.report;return {orgId:this.options.orgId??"self",...this.seatId?{seatId:this.seatId}:{},...this.ci?{ci:true}:{},requestId:n.requestId,
17
+ model:n.model,at:t.at,avoidedLlm:n.calls.avoidedLlm,actualLlm:n.calls.actualLlm,baselineInputTokens:n.tokens.baselineInput,actualInputTokens:n.tokens.actualInput,eliminatedInputTokens:n.tokens.eliminatedInput,
18
+ compressedInputTokens:n.tokens.compressedInput,baselineOutputTokens:n.tokens.baselineOutput,actualOutputTokens:n.tokens.actualOutput,providerCachedInputTokens:n.tokens.providerCachedInput??0,cacheResult:n.
19
+ cache.result,baselineCost:n.cost.baselineEstimate,actualCost:n.cost.actual,netSavings:n.cost.netSavings,...n.cost.basis?{basis:n.cost.basis}:{}}}};function V(){return globalThis.process}function Ve(e){if(/^[0-9a-f]{12}$/.test(e)||/^\d{1,3}(\.\d{1,3}){3}$/.test(e)||/^ip-\d{1,3}(-\d{1,3}){3}$/i.test(e))return true;let t="[bcdfghjklmnpqrstvwxz2456789]";
20
+ return new RegExp(`-${t}{8,10}-${t}{5}$`).test(e)}function Fe(e){return !!(e.AWS_LAMBDA_FUNCTION_NAME??e.LAMBDA_TASK_ROOT??e.VERCEL??e.VERCEL_ENV??e.K_SERVICE??e.FUNCTION_TARGET??e.FUNCTIONS_WORKER_RUNTIME??
21
+ e.NETLIFY)}async function Ue(){if(V()?.execPath)try{let t=await import(["node","os"].join(":")),n=typeof t.hostname=="function"?t.hostname():"";return n.length>0?n:void 0}catch{return}}async function qe(e,t=Ue){
22
+ if(e.KUBERNETES_SERVICE_HOST||Fe(e))return;let n=e.HOSTNAME??e.COMPUTERNAME??await t();if(!(!n||Ve(n)))return n}async function Ke(){let e=V();if(!e||!e.env)return;let t=e.env,n=await qe(t),r=[e.platform,
23
+ e.arch,e.execPath,n,t.USER??t.USERNAME??t.LOGNAME,t.HOME??t.USERPROFILE].filter(o=>typeof o=="string"&&o.length>0);return r.length?r.join("|"):void 0}var He=["CI","CONTINUOUS_INTEGRATION","GITHUB_ACTI\
24
+ ONS","GITLAB_CI","CIRCLECI","TRAVIS","JENKINS_URL","BUILDKITE","TF_BUILD","TEAMCITY_VERSION","BITBUCKET_BUILD_NUMBER","CODEBUILD_BUILD_ID","HEROKU_TEST_RUN_ID","DRONE"];function F(){let e=V()?.env;if(!e)
25
+ return false;for(let t of He){let n=e[t];if(n&&n!=="false"&&n!=="0")return true}return false}async function Je(e,t){let n=globalThis.crypto,r=new TextEncoder;if(n?.subtle)try{let i=await n.subtle.importKey("raw",
26
+ r.encode(e),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),l=await n.subtle.sign("HMAC",i,r.encode(t));return [...new Uint8Array(l)].map(c=>c.toString(16).padStart(2,"0")).join("")}catch{}let o=2166136261,s=`${e}\
27
+ |${t}`;for(let i=0;i<s.length;i++)o^=s.charCodeAt(i),o=Math.imul(o,16777619);return (o>>>0).toString(16).padStart(8,"0")}async function U(e){let n=V()?.env?.BVSALA_SEAT_ID;if(n&&We.test(n))return n;let r=await Ke();
28
+ if(!r)return;let o=await Je(e,r);if(o)return `m-${o.slice(0,32)}`}var We=/^[A-Za-z0-9._:/-]{1,128}$/;var Q={"gpt-4.1":{inputPer1k:.002,outputPer1k:.008,cachedInputPer1k:5e-4},"gpt-4.1-mini":{inputPer1k:4e-4,outputPer1k:.0016,cachedInputPer1k:1e-4},"gpt-4.1-nano":{inputPer1k:1e-4,outputPer1k:4e-4,cachedInputPer1k:25e-6},
29
+ "gpt-4o":{inputPer1k:.0025,outputPer1k:.01,cachedInputPer1k:.00125},"gpt-4o-mini":{inputPer1k:15e-5,outputPer1k:6e-4,cachedInputPer1k:75e-6},"gpt-4-turbo":{inputPer1k:.01,outputPer1k:.03},"gpt-4":{inputPer1k:.03,
30
+ outputPer1k:.06},"gpt-3.5-turbo":{inputPer1k:5e-4,outputPer1k:.0015},"gpt-5":{inputPer1k:.00125,outputPer1k:.01,cachedInputPer1k:125e-6},"gpt-5-codex":{inputPer1k:.00125,outputPer1k:.01,cachedInputPer1k:125e-6},
31
+ "gpt-5-mini":{inputPer1k:25e-5,outputPer1k:.002,cachedInputPer1k:25e-6},"gpt-5-nano":{inputPer1k:5e-5,outputPer1k:4e-4,cachedInputPer1k:5e-6},"gpt-5.1-codex":{inputPer1k:.00125,outputPer1k:.01,cachedInputPer1k:125e-6},
32
+ "gpt-5.1-codex-max":{inputPer1k:.00125,outputPer1k:.01,cachedInputPer1k:125e-6},"gpt-5.1-codex-mini":{inputPer1k:25e-5,outputPer1k:.002,cachedInputPer1k:25e-6},"gpt-5.1-thinking":{inputPer1k:.00125,outputPer1k:.01,
33
+ cachedInputPer1k:125e-6},"gpt-5.2":{inputPer1k:.00175,outputPer1k:.014,cachedInputPer1k:175e-6},"gpt-5.2-codex":{inputPer1k:.00175,outputPer1k:.014,cachedInputPer1k:175e-6},"gpt-5.3-codex":{inputPer1k:.00175,
34
+ outputPer1k:.014,cachedInputPer1k:175e-6},"gpt-5.4":{inputPer1k:.0025,outputPer1k:.015,cachedInputPer1k:25e-5},"gpt-5.4-mini":{inputPer1k:75e-5,outputPer1k:.0045,cachedInputPer1k:75e-6},"gpt-5.4-nano":{
35
+ inputPer1k:2e-4,outputPer1k:.00125,cachedInputPer1k:2e-5},"gpt-5.5":{inputPer1k:.005,outputPer1k:.03,cachedInputPer1k:5e-4},"gpt-5.6-luna":{inputPer1k:2e-4,outputPer1k:.0012,cachedInputPer1k:2e-5},"gp\
36
+ t-5.6-sol":{inputPer1k:.004,outputPer1k:.02,cachedInputPer1k:4e-4},"gpt-5.6-terra":{inputPer1k:.002,outputPer1k:.012,cachedInputPer1k:2e-4},"claude-opus-4-5":{inputPer1k:.005,outputPer1k:.025,cachedInputPer1k:5e-4},
37
+ "claude-opus-4-6":{inputPer1k:.005,outputPer1k:.025,cachedInputPer1k:5e-4},"claude-opus-4-7":{inputPer1k:.005,outputPer1k:.025,cachedInputPer1k:5e-4},"claude-opus-4-8":{inputPer1k:.005,outputPer1k:.025,
38
+ cachedInputPer1k:5e-4},"claude-opus-5":{inputPer1k:.005,outputPer1k:.025,cachedInputPer1k:5e-4},"claude-opus-4":{inputPer1k:.015,outputPer1k:.075,cachedInputPer1k:.0015},"claude-sonnet-4-5":{inputPer1k:.003,
39
+ outputPer1k:.015,cachedInputPer1k:3e-4},"claude-sonnet-4-6":{inputPer1k:.003,outputPer1k:.015,cachedInputPer1k:3e-4},"claude-sonnet-5":{inputPer1k:.002,outputPer1k:.01,cachedInputPer1k:2e-4},"claude-f\
40
+ able-5":{inputPer1k:.01,outputPer1k:.05,cachedInputPer1k:.001},"claude-sonnet-4":{inputPer1k:.003,outputPer1k:.015,cachedInputPer1k:3e-4},"claude-haiku-4-5":{inputPer1k:.001,outputPer1k:.005,cachedInputPer1k:1e-4},
41
+ "claude-3-5-sonnet":{inputPer1k:.003,outputPer1k:.015,cachedInputPer1k:3e-4},"claude-3-5-haiku":{inputPer1k:8e-4,outputPer1k:.004,cachedInputPer1k:8e-5},"claude-3-opus":{inputPer1k:.015,outputPer1k:.075,
42
+ cachedInputPer1k:.0015},"claude-3-haiku":{inputPer1k:25e-5,outputPer1k:.00125,cachedInputPer1k:3e-5},"gemini-3.7-flash":{inputPer1k:75e-5,outputPer1k:.00375,cachedInputPer1k:75e-6},"gemini-3.5-flash":{
43
+ inputPer1k:.0015,outputPer1k:.009,cachedInputPer1k:15e-5},"gemini-3.5-flash-lite":{inputPer1k:3e-4,outputPer1k:.0025,cachedInputPer1k:3e-5},"gemini-3.1-pro-preview":{inputPer1k:.002,outputPer1k:.012,cachedInputPer1k:2e-4},
44
+ "gemini-2.5-pro":{inputPer1k:.00125,outputPer1k:.01,cachedInputPer1k:125e-6},"gemini-2.5-flash":{inputPer1k:3e-4,outputPer1k:.0025,cachedInputPer1k:3e-5}};function ve(e,t,n){if(!n)return;let r=e[n];if(r)
45
+ return r;let o=n.lastIndexOf("/"),s=o>=0?n.slice(o+1):n,i=e[s];if(i)return i;for(let l of t){if(!s.startsWith(l))continue;let c=s.charAt(l.length);if(c!==""&&c!=="-"&&c!=="."&&c!=="_")continue;let p=s.
46
+ slice(l.length+1);if(!(/^\d/.test(p)&&!/^\d{4}/.test(p)))return e[l]}}function X(e){return Object.keys(e).sort((t,n)=>n.length-t.length)}var Ge=X(Q);function M(e){return ve(Q,Ge,e)}var $=class{resto="";uso=null;eventos={};observar(t){this.resto+=typeof t=="string"?t:t.toString("utf8");let n;for(;(n=this.resto.indexOf(`
3
47
 
4
- `))>=0;){let r=this.resto.slice(0,n);this.resto=this.resto.slice(n+2);let o=/^event: (.+)$/m.exec(r)?.[1];if(o&&(this.eventos[o]=(this.eventos[o]??0)+1),o!=="message_start"&&o!=="message_delta")continue;let s=/^data: (.+)$/m.exec(r)?.[1];if(s)try{let i=JSON.parse(s),l=i.usage??i.message?.usage;l&&(this.uso={...this.uso,...l});}catch{}}}usage(){return this.uso}},Re=1.25;function oe(t,e){let n=E(t);if(!n)return null;let r=e.input_tokens??0,o=e.output_tokens??0,s=e.cache_read_input_tokens??0,i=e.cache_creation_input_tokens??0,l=n.cachedInputPer1k??n.inputPer1k;return {costeUsd:r/1e3*n.inputPer1k+s/1e3*l+i/1e3*n.inputPer1k*Re+o/1e3*n.outputPer1k,precio:n,tokens:{entrada:r,salida:o,cacheLeida:s,cacheEscrita:i}}}function re(t,e,n){return ie(t,e,n,false)}function se(t,e,n){return ie(t,e,n,true)}function ie(t,e,n,r){let o=n.tokens.entrada+n.tokens.cacheLeida+n.tokens.cacheEscrita;return {requestId:t,model:e,calls:r?{avoidedLlm:1,actualLlm:0}:{avoidedLlm:0,actualLlm:1},tokens:{baselineInput:o,actualInput:r?0:o,eliminatedInput:0,compressedInput:0,baselineOutput:n.tokens.salida,actualOutput:r?0:n.tokens.salida,providerCachedInput:r?0:n.tokens.cacheLeida},cache:{result:r?"singleflight":"miss"},cost:r?{baselineEstimate:n.costeUsd,actual:0,netSavings:1,basis:"estimated"}:{baselineEstimate:n.costeUsd,actual:n.costeUsd,netSavings:0,basis:"measured"}}}var Ie=5*1024*1024,Pe=["authorization","x-api-key","anthropic-version","anthropic-beta"];function ae(t,e,n,r=""){return createHash("sha256").update(t).update(`
5
- `).update(e).update(`
48
+ `))>=0;){let r=this.resto.slice(0,n);this.resto=this.resto.slice(n+2);let o=/^event: (.+)$/m.exec(r)?.[1];if(o&&(this.eventos[o]=(this.eventos[o]??0)+1),o!=="message_start"&&o!=="message_delta")continue;
49
+ let s=/^data: (.+)$/m.exec(r)?.[1];if(s)try{let i=JSON.parse(s),l=i.usage??i.message?.usage;if(l){let c=l.cache_creation||this.uso?.cache_creation?{...this.uso?.cache_creation,...l.cache_creation}:void 0;
50
+ this.uso={...this.uso,...l,...c?{cache_creation:c}:{}};}}catch{}}}usage(){return this.uso}usageFinal(){return (this.eventos.message_delta??0)>0?this.uso:null}},q=class{resto="";uso=null;eventos={};observar(t){
51
+ this.resto+=typeof t=="string"?t:t.toString("utf8");let n;for(;(n=this.resto.indexOf(`
52
+
53
+ `))>=0;){let r=this.resto.slice(0,n);this.resto=this.resto.slice(n+2);let o=/^event: (.+)$/m.exec(r)?.[1];o&&(this.eventos[o]=(this.eventos[o]??0)+1);let s=/^data: (.+)$/m.exec(r)?.[1];if(s)try{let i=JSON.
54
+ parse(s);if((o??i.type)!=="response.completed")continue;let l=i.response?.usage;l&&(this.uso=Ye(l));}catch{}}}usage(){return this.uso}usageFinal(){return this.uso}};function Ye(e){let t=Math.max(0,e.input_tokens??
55
+ 0),n=Math.min(Math.max(0,e.input_tokens_details?.cached_tokens??0),t),r=Math.min(Math.max(0,e.input_tokens_details?.cache_write_tokens??0),t-n);return {input_tokens:t-n-r,output_tokens:Math.max(0,e.output_tokens??
56
+ 0),cache_read_input_tokens:n,cache_creation_input_tokens:r}}var ye=1.25,Qe=2;function ke(e,t){let n=M(e);if(!n)return null;let r=t.input_tokens??0,o=t.output_tokens??0,s=t.cache_read_input_tokens??0,i=t.
57
+ cache_creation,l=t.cache_creation_input_tokens??(i?.ephemeral_5m_input_tokens??0)+(i?.ephemeral_1h_input_tokens??0),c=Math.min(i?.ephemeral_1h_input_tokens??0,l),p=n.cachedInputPer1k??n.inputPer1k;return {
58
+ costeUsd:r/1e3*n.inputPer1k+s/1e3*p+(l-c)/1e3*n.inputPer1k*Ze(e)+c/1e3*n.inputPer1k*Qe+o/1e3*n.outputPer1k,precio:n,tokens:{entrada:r,salida:o,cacheLeida:s,cacheEscrita:l,cacheEscrita1h:c}}}function xe(e,t){
59
+ try{let n=e,r={maxOutputLength:Xe};switch((t??"").trim().toLowerCase()){case "gzip":case "x-gzip":n=T.gunzipSync(e,r);break;case "deflate":n=T.inflateSync(e,r);break;case "br":n=T.brotliDecompressSync(e,r);
60
+ break;case "zstd":{let s=T.zstdDecompressSync;if(typeof s!="function")return "";n=s(e,r);break}default:break}let o=JSON.parse(n.toString("utf8")).model;return typeof o=="string"?o:""}catch{return ""}}var Xe=5*
61
+ 1024*1024;function Se(){return typeof T.zstdDecompressSync=="function"}function Ze(e){let t=e.toLowerCase();return t.startsWith("gpt-")||t.startsWith("o1")||t.startsWith("o3")||t.startsWith("o4")?/^gpt-5\.6/.
62
+ test(t)?ye:1:t.startsWith("gemini")?1:ye}function Z(e){try{return JSON.parse(typeof e=="string"?e:e.toString("utf8")).usage??null}catch{return null}}function we(e,t,n){return Ce(e,t,n,false)}function Te(e,t,n){
63
+ return Ce(e,t,n,true)}function Ce(e,t,n,r){let o=n.tokens.entrada+n.tokens.cacheLeida+n.tokens.cacheEscrita;return {requestId:e,model:t,calls:r?{avoidedLlm:1,actualLlm:0}:{avoidedLlm:0,actualLlm:1},tokens:{
64
+ baselineInput:o,actualInput:r?0:o,eliminatedInput:0,compressedInput:0,baselineOutput:n.tokens.salida,actualOutput:r?0:n.tokens.salida,providerCachedInput:r?0:n.tokens.cacheLeida},cache:{result:r?"sing\
65
+ leflight":"miss"},cost:r?{baselineEstimate:n.costeUsd,actual:0,netSavings:1,basis:"estimated"}:{baselineEstimate:n.costeUsd,actual:n.costeUsd,netSavings:0,basis:"measured"}}}var ee=5*1024*1024,tt=["authorization","x-api-key","anthropic-version","anthropic-beta"];function Re(e,t,n,r=""){return createHash("sha256").update(e).update(`
66
+ `).update(t).update(`
6
67
  `).update(r).update(`
7
- `).update(n).digest("hex")}function ce(t){return Pe.map(e=>`${e}=${t(e)??""}`).join(`
8
- `)}var _=class{enVuelo=new Map;primarias=0;gemelas=0;registrar(e){if(this.enVuelo.has(e))return null;this.primarias+=1;let n,r=new Promise(l=>n=l);this.enVuelo.set(e,{promesa:r,resolver:n});let o=[],s=0,i=false;return {capturar:l=>{if(!i){if(s+=l.length,s>Ie){i=true,o.length=0;return}o.push(l);}},terminar:(l,u)=>{let a=!i&&l>=200&&l<300;this.enVuelo.get(e)?.resolver(a?{status:l,headers:u,chunks:o}:null),this.enVuelo.delete(e);},abortar:()=>{this.enVuelo.get(e)?.resolver(null),this.enVuelo.delete(e);}}}async esperar(e){let n=this.enVuelo.get(e);if(!n)return null;let r=await n.promesa;return r&&(this.gemelas+=1),r}tieneEnVuelo(e){return this.enVuelo.has(e)}};var Ae=["content-encoding","content-length","transfer-encoding"];async function le(t={}){let e=(t.upstream??"https://api.anthropic.com").replace(/\/+$/,""),n=t.fetchImpl??fetch,r=t.log??(a=>console.log(a)),o;if(t.serviceAccountKey){o=new w({url:t.meteringUrl??"https://savings.bivelio.com/api/metering/usage",serviceAccountKey:t.serviceAccountKey,...t.fetchImpl?{fetchImpl:t.fetchImpl}:{},onAuthError:c=>v("gateway-ingest-auth",`[bvsala gateway] la ingesta de uso rechaza la credencial (${c}). El tr\xE1fico sigue fluyendo (avisar, nunca romper), pero este uso no est\xE1 llegando al panel. Revisa BIVELIO_SERVICE_KEY y el estado de la suscripci\xF3n en savings.bivelio.com.`)});let a=D();t.seatId?o.setSeat(t.seatId,a):A(t.serviceAccountKey).then(c=>o?.setSeat(c,a)).catch(()=>{}),o.start();}else v("gateway-sin-clave","[bvsala gateway] sin BIVELIO_SERVICE_KEY: el gateway reenv\xEDa el tr\xE1fico pero NO mide. Consigue tu clave en savings.bivelio.com/dashboard/install para ver el gasto en tu panel.");let s=t.coalescer?new _:null,i=(a,c,m,f,g)=>{if(!o||!m)return;let d="";try{d=JSON.parse(c.toString("utf8")).model??"";}catch{}let p=d?oe(d,m):null;p?(o.record(f?se(a,d,p):re(a,d,p)),r(`[gateway] POST ${g} ${f?"\xB7 COALESCIDA (llamada evitada)":""} \xB7 ${d} \xB7 $${p.costeUsd.toFixed(6)} (in ${p.tokens.entrada}, cache\u2191${p.tokens.cacheEscrita} cache\u2193${p.tokens.cacheLeida}, out ${p.tokens.salida})`)):d&&v(`gateway-sin-precio:${d}`,`[bvsala gateway] el modelo "${d}" no est\xE1 en el cat\xE1logo de precios; su uso se reenv\xEDa pero no se mide.`);},l=createServer(async(a,c)=>{let m=randomUUID(),f=new AbortController;c.on("close",()=>{c.writableEnded||f.abort();});let g=null,d=new URL(a.url??"/",e);try{let p=[];for await(let h of a)p.push(h);let y=Buffer.concat(p);d=new URL(a.url??"/",e);let C=s!==null&&a.method==="POST"&&y.length>0,N=ce(h=>{let b=a.headers[h];return Array.isArray(b)?b.join(", "):b}),R=C?ae("POST",d.pathname+d.search,y,N):"";if(C)if(s.tieneEnVuelo(R)){let h=await s.esperar(R);if(h){c.writeHead(h.status,h.headers);let b=new I;for(let ee of h.chunks)c.write(ee),b.observar(ee);c.end(),i(m,y,b.usage(),!0,d.pathname);return}}else g=s.registrar(R);let X={};for(let[h,b]of Object.entries(a.headers))h==="host"||h==="content-length"||b===void 0||(X[h]=Array.isArray(b)?b.join(", "):b);let k;try{k=await n(d,{method:a.method,headers:X,body:["GET","HEAD"].includes(a.method??"")?void 0:y,redirect:"manual",signal:f.signal});}catch(h){g?.abortar(),c.headersSent||(c.writeHead(502,{"content-type":"application/json"}),c.end(JSON.stringify({error:"bvsala gateway: upstream unreachable",detail:String(h)}))),r(`[gateway] ${a.method} ${d.pathname} \u2192 upstream inalcanzable: ${String(h)}`);return}let z=Object.fromEntries(k.headers.entries());for(let h of Ae)delete z[h];c.writeHead(k.status,z);let Z=(k.headers.get("content-type")??"").includes("text/event-stream"),B=Z?new I:null;if(k.body)for await(let h of k.body){let b=Buffer.from(h);c.write(b),B?.observar(b),g?.capturar(b);}c.end(),g?.terminar(k.status,z),B&&k.ok?i(m,y,B.usage(),!1,d.pathname):Z||r(`[gateway] ${a.method} ${d.pathname} \u2192 ${k.status}`);}catch(p){if(g?.abortar(),f.signal.aborted)r(`[gateway] ${a.method} ${d.pathname} \u2192 el cliente abort\xF3; upstream cancelado`);else if(r(`[gateway] ${a.method} ${d.pathname} \u2192 error sirviendo: ${String(p)}`),!c.headersSent){c.writeHead(502,{"content-type":"application/json"}),c.end(JSON.stringify({error:"bvsala gateway: stream interrupted",detail:String(p)}));return}c.writableEnded||c.end();}});return await new Promise((a,c)=>{l.once("error",c),l.listen(t.puerto??8402,"127.0.0.1",()=>a());}),{puerto:l.address().port,cerrar:async()=>{await o?.stopAndDrain(),await new Promise(a=>l.close(()=>a()));}}}var U=class{id;exact=false;constructor(e="heuristic"){this.id=e;}count(e){if(e.length===0)return 0;let n=e.match(/[A-Za-z]+|\d+|\s+|[^\sA-Za-z\d]/g)??[],r=0;for(let o of n){let s=o[0];if(/\s/.test(s)){let i=(o.match(/\n/g)??[]).length;r+=i+Math.floor(Math.max(0,o.length-1)/2);}else /[A-Za-z]/.test(s)?r+=Math.max(1,Math.ceil(o.length/4)):/\d/.test(s)?r+=Math.max(1,Math.ceil(o.length/2)):r+=1;}return Math.max(r,Math.ceil(e.length/4))}},q=new U;var K=class{estimators=new Map;modelToTokenizer=new Map;fallbackId;constructor(e=q){this.fallbackId=e.id,this.estimators.set(e.id,e);}register(e){return this.estimators.set(e.id,e),this}mapModel(e,n){return this.modelToTokenizer.set(e,n),this}resolve(e){let n=e.tokenizer?this.estimators.get(e.tokenizer):void 0;if(n)return n;if(e.model){let r=this.modelToTokenizer.get(e.model);if(r){let o=this.estimators.get(r);if(o)return o}}return this.estimators.get(this.fallbackId)}count(e,n){return this.resolve(n).count(e)}};new K;new TextEncoder;new TextDecoder;new TextDecoder;var We={"gpt-4o-mini":{deltaTokens:-16,salidaDelBrazoA:72,contrato:"triage (6 fields, long enums)",pares:8},"gpt-4o":{deltaTokens:3,salidaDelBrazoA:75,contrato:"triage (6 fields, long enums)",pares:8}};V(We);var G="0.4.3";var[,,x,...j]=process.argv;function Ye(){console.log(`bvsala ${G} \u2014 BiVelio Savings Layer
68
+ `).update(n).digest("hex")}function Ee(e){return tt.map(t=>`${t}=${e(t)??""}`).join(`
69
+ `)}var K=class{enVuelo=new Map;primarias=0;gemelas=0;registrar(t){if(this.enVuelo.has(t))return null;this.primarias+=1;let n,r=new Promise(l=>n=l);this.enVuelo.set(t,{promesa:r,resolver:n});let o=[],s=0,
70
+ i=false;return {capturar:l=>{if(!i){if(s+=l.length,s>ee){i=true,o.length=0;return}o.push(l);}},terminar:(l,c)=>{let p=!i&&l>=200&&l<300;this.enVuelo.get(t)?.resolver(p?{status:l,headers:c,chunks:o}:null),this.
71
+ enVuelo.delete(t);},abortar:()=>{this.enVuelo.get(t)?.resolver(null),this.enVuelo.delete(t);}}}async esperar(t){let n=this.enVuelo.get(t);if(!n)return null;let r=await n.promesa;return r&&(this.gemelas+=
72
+ 1),r}tieneEnVuelo(t){return this.enVuelo.has(t)}};var rt=["content-encoding","content-length","transfer-encoding"];async function Ie(e={}){let t=(e.upstream??"https://api.anthropic.com").replace(/\/+$/,""),n=e.fetchImpl??fetch,r=e.log??(a=>console.log(
73
+ a)),o;if(e.serviceAccountKey){o=new E({url:e.meteringUrl??"https://savings.bivelio.com/api/metering/usage",serviceAccountKey:e.serviceAccountKey,...e.fetchImpl?{fetchImpl:e.fetchImpl}:{},onAuthError:u=>S(
74
+ "gateway-ingest-auth",`[bvsala gateway] la ingesta de uso rechaza la credencial (${u}). El tr\xE1fico sigue fluyendo (avisar, nunca romper), pero este uso no est\xE1 llegando al panel. Revisa BIVELIO_SERVIC\
75
+ E_KEY y el estado de la suscripci\xF3n en savings.bivelio.com.`)});let a=F();e.seatId?o.setSeat(e.seatId,a):U(e.serviceAccountKey).then(u=>o?.setSeat(u,a)).catch(()=>{}),o.start();}else S("gateway-sin-\
76
+ clave","[bvsala gateway] sin BIVELIO_SERVICE_KEY: el gateway reenv\xEDa el tr\xE1fico pero NO mide. Consigue tu clave en savings.bivelio.com/dashboard/install para ver el gasto en tu panel.");let s=e.
77
+ coalescer?new K:null,i=a=>a==="/v1/messages"?new $:a.endsWith("/responses")?new q:null,l=(a,u,f,b,m,d)=>{if(!o||!f)return;let h=xe(u,d);if(!h){S(`gateway-sin-modelo:${m}`,`[bvsala gateway] no se pudo \
78
+ leer el modelo del cuerpo de ${m} (content-encoding: ${d??"ninguno"}); su uso se reenv\xEDa pero no se mide.`);return}let y=ke(h,f);y?(o.record(b?Te(a,h,y):we(a,h,y)),r(`[gateway] POST ${m} ${b?"\xB7 COA\
79
+ LESCIDA (llamada evitada)":""} \xB7 ${h} \xB7 $${y.costeUsd.toFixed(6)} (in ${y.tokens.entrada}, cache\u2191${y.tokens.cacheEscrita}${y.tokens.cacheEscrita1h?` (1h ${y.tokens.cacheEscrita1h})`:""} cac\
80
+ he\u2193${y.tokens.cacheLeida}, out ${y.tokens.salida})`)):S(`gateway-sin-precio:${h}`,`[bvsala gateway] el modelo "${h}" no est\xE1 en el cat\xE1logo de precios; su uso se reenv\xEDa pero no se mide.`);},
81
+ c=createServer(async(a,u)=>{let f=randomUUID(),b=new AbortController;u.on("close",()=>{u.writableEnded||b.abort();});let m=null,d=new URL(a.url??"/",t),h=null,y=Buffer.alloc(0),z=false,O=a.headers["content-encoding"],B=Array.
82
+ isArray(O)?O[0]:O;try{let D=[];for await(let g of a)D.push(g);let w=Buffer.concat(D);y=w,d=new URL(a.url??"/",t);let pe=s!==null&&a.method==="POST"&&w.length>0,ze=Ee(g=>{let v=a.headers[g];return Array.
83
+ isArray(v)?v.join(", "):v}),W=pe?Re("POST",d.pathname+d.search,w,ze):"";if(pe)if(s.tieneEnVuelo(W)){let g=await s.esperar(W);if(g){u.writeHead(g.status,g.headers);let v=i(d.pathname)??new $;for(let ge of g.
84
+ chunks)u.write(ge),v.observar(ge);u.end();let Be=v.usage()??(d.pathname==="/v1/messages"?Z(Buffer.concat(g.chunks)):null);l(f,w,Be,!0,d.pathname,B);return}}else m=s.registrar(W);let me={};for(let[g,v]of Object.
85
+ entries(a.headers))g==="host"||g==="content-length"||v===void 0||(me[g]=Array.isArray(v)?v.join(", "):v);let x;try{x=await n(d,{method:a.method,headers:me,body:["GET","HEAD"].includes(a.method??"")?void 0:
86
+ w,redirect:"manual",signal:b.signal});}catch(g){m?.abortar(),u.headersSent||(u.writeHead(502,{"content-type":"application/json"}),u.end(JSON.stringify({error:"bvsala gateway: upstream unreachable",detail:String(
87
+ g)}))),r(`[gateway] ${a.method} ${d.pathname} \u2192 upstream inalcanzable: ${String(g)}`);return}let G=Object.fromEntries(x.headers.entries());for(let g of rt)delete G[g];u.writeHead(x.status,G);let fe=x.
88
+ headers.get("content-type")??"",Y=fe.includes("text/event-stream")||fe===""&&x.ok&&i(d.pathname)!==null;h=Y?i(d.pathname):null;let L=!Y&&x.ok&&o!==void 0&&a.method==="POST"&&d.pathname==="/v1/messages"&&
89
+ (x.headers.get("content-type")??"").includes("application/json")?[]:null,he=0;if(x.body)for await(let g of x.body){let v=Buffer.from(g);u.write(v),h?.observar(v),L&&(he+=v.length,he>ee?L=null:L.push(v)),
90
+ m?.capturar(v);}u.end(),m?.terminar(x.status,G),h&&x.ok?(z=!0,l(f,w,h.usage(),!1,d.pathname,B)):L?l(f,w,Z(Buffer.concat(L)),!1,d.pathname,B):Y||r(`[gateway] ${a.method} ${d.pathname} \u2192 ${x.status}`);}catch(D){
91
+ if(m?.abortar(),b.signal.aborted){let w=!z&&h?h.usageFinal():null;w?(r(`[gateway] ${a.method} ${d.pathname} \u2192 el cliente cerr\xF3 tras el usage final; se mide igual`),l(f,y,w,false,d.pathname,B)):r(
92
+ `[gateway] ${a.method} ${d.pathname} \u2192 el cliente abort\xF3; upstream cancelado`);}else if(r(`[gateway] ${a.method} ${d.pathname} \u2192 error sirviendo: ${String(D)}`),!u.headersSent){u.writeHead(
93
+ 502,{"content-type":"application/json"}),u.end(JSON.stringify({error:"bvsala gateway: stream interrupted",detail:String(D)}));return}u.writableEnded||u.end();}});return c.on("upgrade",(a,u)=>{r(`[gatew\
94
+ ay] ${a.method} ${new URL(a.url??"/","http://x").pathname} \u2192 426 (WebSocket no soportado; el cliente cae a HTTP)`),u.once("error",()=>{}),u.once("finish",()=>u.destroy()),u.end(`HTTP/1.1 426 Upgr\
95
+ ade Required\r
96
+ connection: close\r
97
+ content-length: 0\r
98
+ \r
99
+ `);}),await new Promise((a,u)=>{c.once("error",u),c.listen(e.puerto??8402,"127.0.0.1",()=>a());}),{puerto:c.address().port,cerrar:async()=>{await o?.stopAndDrain(),await new Promise(a=>c.close(()=>a()));}}}var te=["claude","codex","goose","qwen"];function Pe(e){return te.includes(e??"")}function _e(e={}){let n=(e.env??process.env).BVSALA_CODEX_AUTH?.toLowerCase();if(n==="api-key"||n==="apikey")return "ap\
100
+ i-key";if(n==="chatgpt")return "chatgpt";let r=null;try{r=e.leerAuth?e.leerAuth():null;}catch{r=null;}if(r===null)return "api-key";try{let o=JSON.parse(r);return typeof o.auth_mode=="string"?o.auth_mode.toLowerCase().
101
+ replace(/[^a-z]/g,"")==="apikey"?"api-key":"chatgpt":o.tokens!==void 0&&o.tokens!==null?"chatgpt":"api-key"}catch{return "api-key"}}var st={"api-key":{upstream:"https://api.openai.com",prefijo:"/v1"},chatgpt:{
102
+ upstream:"https://chatgpt.com",prefijo:"/backend-api/codex"}};function ne(e,t,n,r={}){let o=`http://127.0.0.1:${t}`;switch(e){case "claude":return {bin:"claude",args:n,env:{ANTHROPIC_BASE_URL:o}};case "g\
103
+ oose":return {bin:"goose",args:n,env:{ANTHROPIC_HOST:o}};case "qwen":return {bin:"qwen",args:n,env:{ANTHROPIC_BASE_URL:o},nota:'Qwen Code solo enruta por aqu\xED con `security.auth.selectedType: "anthropic"\
104
+ ` en ~/.qwen/settings.json (ver `bvsala doctor`).'};case "codex":{let s=r.modoCodex??"api-key",i=st[s];return {bin:"codex",args:["-c",`openai_base_url="${o}${i.prefijo}"`,...n],env:{},upstream:i.upstream,
105
+ nota:s==="chatgpt"?"Auth ChatGPT detectada: el gateway enruta a chatgpt.com/backend-api/codex. El tr\xE1fico de suscripci\xF3n se mide (equivalente a precio de lista) pero no se monetiza: no hay factura por\
106
+ tokens.":"Auth por API key: el gateway enruta a api.openai.com/v1."}}}}var lt="comprobar: en el log del gateway debe aparecer `[gateway] POST /v1/messages \u2026`; si ve\
107
+ s `/v1/v1/messages`, sobra un /v1; si ves `POST /messages` a secas, falta uno.",ut=[{id:"claude",nombre:"Claude Code",binario:"claude",configs:[],detectable:true,receta:({base:e})=>["usa `bvsala claude`\
108
+ \u2014 arranca el gateway en un puerto ef\xEDmero, lanza `claude` apunt\xE1ndole y lo apaga al salir.",`a mano: ANTHROPIC_BASE_URL=${e} claude (sin /v1: el cliente a\xF1ade /v1/messages)`]},{id:"co\
109
+ dex",nombre:"Codex CLI",binario:"codex",configs:[join(".codex","config.toml"),join(".codex","auth.json")],detectable:true,receta:({base:e,rutas:t})=>["usa `bvsala codex` \u2014 el prefijo del base URL depende del \
110
+ modo de auth y lo resuelve el envoltorio:",` API key \u2192 openai_base_url=${e}/v1`,` suscripci\xF3n ChatGPT \u2192 openai_base_url=${e}/backend-api/codex (tiene que terminar exactament\
111
+ e ah\xED)`,`${t[1]}: solo se comprueba que exista; nunca se lee.`]},{id:"opencode",nombre:"opencode",binario:"opencode",configs:[join(".config","opencode","opencode.json")],detectable:true,receta:({baseV1:e,
112
+ rutas:t})=>[`mecanismo: ${t[0]} \u2192 provider.anthropic.options.baseURL (el opencode.json del proyecto pisa al global). CON /v1.`,"{",' "provider": {',' "anthropic": {',` "options": { "base\
113
+ URL": "${e}" }`," }"," }","}",'alternativa: "baseURL": "{env:BVSALA_BASE_URL}" y exportar la variable; trampa: sin la variable, baseURL queda vac\xEDo.']},{id:"goose",nombre:"Goose",binario:"goose",
114
+ configs:[join(".config","goose","config.yaml")],detectable:true,receta:({base:e,rutas:t})=>[`mecanismo: variable de entorno ANTHROPIC_HOST (pisa ${t[0]}). SIN /v1.`,`ANTHROPIC_HOST=${e} ANTHROPIC_API_KEY=\u2026\
115
+ goose`]},{id:"crush",nombre:"Crush",binario:"crush",configs:[join(".config","crush","crushrc")],detectable:true,receta:({baseV1:e,rutas:t})=>[`mecanismo: ${t[0]} (Bash); crush.json est\xE1 deprecado. CON /v1\
116
+ y SIN barra final (el SDK Go resuelve relativo: /v1/ \u2192 /v1/v1/messages).`,`provider add bvsala --type anthropic --base-url ${e} --api-key "$ANTHROPIC_API_KEY"`]},{id:"qwen",nombre:"Qwen Code",binario:"\
117
+ qwen",configs:[join(".qwen","settings.json")],detectable:true,receta:({base:e,rutas:t})=>[`mecanismo: variable de entorno ANTHROPIC_BASE_URL + ${t[0]} con selectedType = anthropic. SIN /v1 (el SDK TS ofici\
118
+ al a\xF1ade /v1/messages).`,`ANTHROPIC_BASE_URL=${e} ANTHROPIC_API_KEY=\u2026 qwen`,`comprueba a mano que ${t[0]} lleve selectedType: anthropic \u2014 el doctor no lee el archivo.`]},{id:"droid",nombre:"\
119
+ Droid (Factory)",binario:"droid",configs:[join(".factory","settings.json")],detectable:true,receta:({base:e,rutas:t})=>[`mecanismo: ${t[0]} \u2192 customModels[] con provider "anthropic". SIN /v1 [inferido: val\
120
+ idar en mano contra el log del gateway].`,"{",' "customModels": ['," {",' "model": "<id de modelo Anthropic>",',' "displayName": "<nombre visible>",',' "provider": "anthropic",',` \
121
+ "baseUrl": "${e}",`,' "apiKey": "<tu API key de Anthropic>"'," }"," ]","}"]},{id:"cline",nombre:"Cline",configs:[],detectable:false,receta:({baseV1:e})=>["mecanismo: UI \u2192 proveedor Anthropic\
122
+ \u2192 marca \xABUse custom base URL\xBB. CON /v1 (Cline fija https://api.anthropic.com/v1 y solo normaliza la URL oficial exacta).",`Base URL: ${e}`]},{id:"kilo",nombre:"Kilo Code",configs:[],detectable:false,
123
+ receta:({baseV1:e})=>["mecanismo: UI \u2192 proveedor custom, Provider API \xABAnthropic Messages\xBB. Probar CON /v1 primero (linaje Cline) [inferido: validar en mano].",`Base URL: ${e}`]}];function dt(e,t){
124
+ let n=e.split(delimiter).filter(o=>o.length>0),r=process.platform==="win32"?["",".exe",".cmd",".bat"]:[""];return o=>n.some(s=>r.some(i=>t(join(s,o+i))))}async function Ae(e,t={}){if(!Number.isInteger(e)||e<1||
125
+ e>65535)throw new RangeError(`[bvsala doctor] puerto inv\xE1lido: ${String(e)}`);let n=t.home??homedir(),r=t.existe??existsSync,o=t.enPath??dt(t.path??process.env.PATH??"",r),s=`http://127.0.0.1:${e}`,i=`${s}/v1`,
126
+ l=[],c=[`bvsala doctor \u2014 gateway local en ${s}`,"Solo imprime: no escribe ninguna configuraci\xF3n. Detecci\xF3n por existencia (binarios en PATH y","archivos de configuraci\xF3n de usuario); nunca \
127
+ se lee el contenido de ning\xFAn archivo.",""];for(let p of ut){let a=p.configs.map(d=>join(n,d)),u=p.binario!==void 0&&o(p.binario),f=a.filter(d=>r(d)),b=u||f.length>0;b&&l.push(p.id);let m;p.detectable?
128
+ b?m=`detectado \u2014 ${[...u?[`binario \`${p.binario}\` en PATH`]:[],...f.map(h=>`config presente: ${h}`)].join("; ")}`:m="no detectado":m="sin detecci\xF3n autom\xE1tica (extensi\xF3n de editor)",c.
129
+ push(`\xBB ${p.id}: ${m}`),c.push(` ${p.nombre}`);for(let d of p.receta({base:s,baseV1:i,rutas:a}))c.push(` ${d}`);c.push(` ${lt}`),c.push("");}return c.push("Regla BYOK: las recetas del anillo Anth\
130
+ ropic (Goose, Qwen Code, opencode, Crush, Droid, Cline, Kilo) son","para API key. Nunca enrutes por el gateway el OAuth de una suscripci\xF3n Claude Pro/Max desde un harness","de terceros \u2014 Anthropic \
131
+ lo proh\xEDbe. Claude Code y Codex con suscripci\xF3n se enrutan y se miden, pero no","se monetizan: no hay factura por tokens.","Detalle por terminal: docs/terminales/<terminal>.md"),{texto:c.join(`
132
+ `),detectados:l}}var oe=class{id;exact=false;constructor(t="heuristic"){this.id=t;}count(t){if(t.length===0)return 0;let n=t.match(/[A-Za-z]+|\d+|\s+|[^\sA-Za-z\d]/g)??[],r=0;for(let o of n){let s=o[0];if(/\s/.test(s)){let i=(o.
133
+ match(/\n/g)??[]).length;r+=i+Math.floor(Math.max(0,o.length-1)/2);}else /[A-Za-z]/.test(s)?r+=Math.max(1,Math.ceil(o.length/4)):/\d/.test(s)?r+=Math.max(1,Math.ceil(o.length/2)):r+=1;}return Math.max(r,
134
+ Math.ceil(t.length/4))}},re=new oe;var se=class{estimators=new Map;modelToTokenizer=new Map;fallbackId;constructor(t=re){this.fallbackId=t.id,this.estimators.set(t.id,t);}register(t){return this.estimators.set(t.id,t),this}mapModel(t,n){
135
+ return this.modelToTokenizer.set(t,n),this}resolve(t){let n=t.tokenizer?this.estimators.get(t.tokenizer):void 0;if(n)return n;if(t.model){let r=this.modelToTokenizer.get(t.model);if(r){let o=this.estimators.
136
+ get(r);if(o)return o}}return this.estimators.get(this.fallbackId)}count(t,n){return this.resolve(n).count(t)}};new se;new TextEncoder;new TextDecoder;new TextDecoder;var Et={"gpt-4o-mini":{deltaTokens:-16,salidaDelBrazoA:72,contrato:"triage (6 fields, long enums)",pares:8},"gpt-4o":{deltaTokens:3,salidaDelBrazoA:75,contrato:"triage (6 fields, long enums)",pares:8}};
137
+ X(Et);var le="0.4.5";var[,,k,...A]=process.argv;function Ot(){console.log(`bvsala ${le} \u2014 BiVelio Savings Layer
9
138
 
10
139
  Comandos:
11
- gateway [--puerto N] Proxy local medidor para Claude Code (ANTHROPIC_BASE_URL).
140
+ gateway [--puerto N] Proxy local medidor (por defecto 8402) para terminales que
141
+ respetan un base URL: Claude Code, opencode, Goose, Crush\u2026
12
142
  claude [args\u2026] Lanza Claude Code a trav\xE9s del gateway, sin configurar nada.
143
+ codex [args\u2026] Lanza Codex CLI a trav\xE9s del gateway (API key: api.openai.com;
144
+ auth ChatGPT: chatgpt.com/backend-api/codex \u2014 se mide, no se monetiza).
145
+ goose [args\u2026] Lanza Goose (ANTHROPIC_HOST) a trav\xE9s del gateway.
146
+ qwen [args\u2026] Lanza Qwen Code (ANTHROPIC_BASE_URL) a trav\xE9s del gateway.
147
+ doctor [--puerto N] Detecta terminales instalados e imprime su configuraci\xF3n exacta.
13
148
 
14
149
  Entorno:
15
150
  BIVELIO_SERVICE_KEY Clave de servicio (bvk_\u2026): habilita metering + asiento.
16
151
  BVSALA_SEAT_ID Identidad de asiento estable por r\xE9plica (opcional).
17
- BVSALA_GATEWAY_UPSTREAM Upstream alternativo (por defecto https://api.anthropic.com).
152
+ BVSALA_GATEWAY_UPSTREAM Upstream alternativo (por defecto el del terminal).
18
153
  BVSALA_GATEWAY_COALESCE =1 enciende la coalescencia de duplicados (F2, experimental).
154
+ BVSALA_CODEX_AUTH api-key | chatgpt \u2014 fuerza el modo de auth de Codex.
155
+ CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 (Claude Code, solo API key) tus modelos en /model.
19
156
 
20
157
  El gateway nunca corta el tr\xE1fico: sin clave o con la ingesta ca\xEDda, reenv\xEDa y avisa.
21
- Tus prompts jam\xE1s salen de tu m\xE1quina \u2014 solo cuentas y coste. M\xE1s: savings.bivelio.com`);}x!=="gateway"&&x!=="claude"&&(Ye(),process.exit(x===void 0||x==="help"||x==="--help"?0:1));var Y=j.indexOf("--puerto"),$=x==="claude"?0:Y>=0?Number(j[Y+1]):8402;(!Number.isInteger($)||$<0||$>65535)&&(console.error(`[bvsala gateway] puerto inv\xE1lido: ${j[Y+1]}`),process.exit(1));var Q=process.env.BIVELIO_SERVICE_KEY,T=await le({puerto:$,...Q?{serviceAccountKey:Q}:{},...process.env.BVSALA_SEAT_ID?{seatId:process.env.BVSALA_SEAT_ID}:{},...process.env.BVSALA_GATEWAY_UPSTREAM?{upstream:process.env.BVSALA_GATEWAY_UPSTREAM}:{},...process.env.BVSALA_GATEWAY_COALESCE==="1"?{coalescer:true}:{},...process.env.BVSALA_METERING_URL?{meteringUrl:process.env.BVSALA_METERING_URL}:{}});console.log(`[bvsala gateway] v${G} escuchando en http://127.0.0.1:${T.puerto}`);console.log(`[bvsala gateway] medici\xF3n: ${Q?"ACTIVA (clave de servicio presente)":"APAGADA (falta BIVELIO_SERVICE_KEY)"}`);if(x==="claude"){let t=spawn("claude",j,{stdio:"inherit",env:{...process.env,ANTHROPIC_BASE_URL:`http://127.0.0.1:${T.puerto}`}});t.on("error",e=>{console.error(`[bvsala claude] no se pudo lanzar \`claude\`: ${String(e)}. \xBFEst\xE1 instalado y en el PATH?`),T.cerrar().then(()=>process.exit(127));}),t.on("exit",(e,n)=>{T.cerrar().then(()=>process.exit(e??(n?1:0)));});}else console.log(`[bvsala gateway] apunta tu herramienta: ANTHROPIC_BASE_URL=http://127.0.0.1:${T.puerto} claude`);var ge=async()=>{await T.cerrar(),process.exit(0);};process.on("SIGINT",()=>{ge();});process.on("SIGTERM",()=>{ge();});
158
+ Tus prompts jam\xE1s salen de tu m\xE1quina \u2014 solo cuentas y coste. M\xE1s: savings.bivelio.com`);}var N=Pe(k);k!=="gateway"&&k!=="doctor"&&!N&&(Ot(),process.exit(k===void 0||k==="help"||k==="--he\
159
+ lp"?0:1));var ue=A.indexOf("--puerto"),P=N?0:ue>=0?Number(A[ue+1]):8402;(!Number.isInteger(P)||(N?P<0:P<1)||P>65535)&&(console.error(`[bvsala ${k}] puerto inv\xE1lido: ${A[ue+1]}`),process.exit(1));if(k===
160
+ "doctor"){A.includes("--write")&&(console.error("[bvsala doctor] --write todav\xEDa no existe: el doctor solo imprime; copia el bloque a mano."),process.exit(1));let e=await Ae(P);console.log(e.texto),
161
+ process.exit(0);}var R=N?ne(k,0,A,{modoCodex:k==="codex"?_e({leerAuth:()=>readFileSync(join(homedir(),".codex","auth.json"),"utf8")}):void 0}):null,de=process.env.BIVELIO_SERVICE_KEY,_=await Ie({puerto:P,...de?{serviceAccountKey:de}:
162
+ {},...process.env.BVSALA_SEAT_ID?{seatId:process.env.BVSALA_SEAT_ID}:{},...process.env.BVSALA_GATEWAY_UPSTREAM?{upstream:process.env.BVSALA_GATEWAY_UPSTREAM}:R?.upstream?{upstream:R.upstream}:{},...process.
163
+ env.BVSALA_GATEWAY_COALESCE==="1"?{coalescer:true}:{},...process.env.BVSALA_METERING_URL?{meteringUrl:process.env.BVSALA_METERING_URL}:{}}),Dt=process.env.BVSALA_GATEWAY_UPSTREAM??R?.upstream??"https://\
164
+ api.anthropic.com";console.log(`[bvsala gateway] v${le} escuchando en http://127.0.0.1:${_.puerto} \u2192 ${Dt}`);console.log(`[bvsala gateway] medici\xF3n: ${de?"ACTIVA (clave de servicio presente)":
165
+ "APAGADA (falta BIVELIO_SERVICE_KEY)"}`);process.env.BVSALA_GATEWAY_UPSTREAM&&R?.upstream&&process.env.BVSALA_GATEWAY_UPSTREAM!==R.upstream&&console.error(`[bvsala ${k}] AVISO: BVSALA_GATEWAY_UPSTREAM\
166
+ =${process.env.BVSALA_GATEWAY_UPSTREAM} pisa el upstream de este terminal (${R.upstream}). Si no es lo que quieres, quita la variable.`);k==="codex"&&!Se()&&console.error(`[bvsala codex] AVISO: este N\
167
+ ode (${process.version}) no descomprime zstd y Codex manda el cuerpo en zstd: el tr\xE1fico pasa pero NO se mide. Usa Node \u2265 22.15.`);if(R&&N){let e=ne(k,_.puerto,A,{...R.upstream?{modoCodex:R.upstream.
168
+ includes("chatgpt.com")?"chatgpt":"api-key"}:{}});e.nota&&console.log(`[bvsala ${k}] ${e.nota}`);let t=spawn(e.bin,e.args,{stdio:"inherit",env:{...process.env,...e.env}});t.on("error",n=>{console.error(`\
169
+ [bvsala ${k}] no se pudo lanzar \`${e.bin}\`: ${String(n)}. \xBFEst\xE1 instalado y en el PATH?`),_.cerrar().then(()=>process.exit(127));}),t.on("exit",(n,r)=>{_.cerrar().then(()=>process.exit(n??(r?1:
170
+ 0)));});}else console.log(`[bvsala gateway] apunta tu terminal (o usa \`bvsala ${te.join("|")}\`): ANTHROPIC_BASE_URL=http://127.0.0.1:${_.puerto} claude \xB7 bvsala doctor para el resto`);var Ne=async()=>{
171
+ await _.cerrar(),process.exit(0);};process.on("SIGINT",()=>{Ne();});process.on("SIGTERM",()=>{Ne();});
package/dist/index.d.ts CHANGED
@@ -45,6 +45,10 @@ declare class SavingsLedger {
45
45
  actualOutput: number;
46
46
  eliminatedOutput: number;
47
47
  providerCachedInput: number;
48
+ /** Tokens the provider billed as prefix-cache writes. */
49
+ cacheWriteInputTokens: number;
50
+ /** The premium on those writes, already priced in USD. Adds to `actual` only. */
51
+ cacheWritePremiumUsd: number;
48
52
  /** Cuenta LOCAL del prompt que de verdad se envió al proveedor. */
49
53
  sentInputLocal: number;
50
54
  /** Tokens que la capa quitó de ese prompt, en cuenta LOCAL. */
@@ -98,6 +102,8 @@ declare class SavingsLedger {
98
102
  inputTokens: number;
99
103
  outputTokens: number;
100
104
  }, cheapPrice: CostModel): void;
105
+ /** Record the premium for `tokens` of cache write billed at `multiplicador`x. */
106
+ cacheWritePremium(tokens: number, multiplicador: number, precioDelQueMarco: CostModel): void;
101
107
  /**
102
108
  * Lo que de verdad hizo la compresión, para publicarlo en
103
109
  * `SavingsReport.compression`. El campo estaba declarado en el contrato y no lo
@@ -207,6 +213,24 @@ interface ChatMessage {
207
213
  content: string;
208
214
  /** Optional name (e.g. tool name for role: "tool"). */
209
215
  name?: string;
216
+ /**
217
+ * For `role: "tool"`: the id of the tool call this message answers.
218
+ *
219
+ * The provider tool loop needs it: OpenAI's Chat Completions rejects a
220
+ * `role: "tool"` message that does not carry `tool_call_id` and does not follow
221
+ * an assistant turn with `tool_calls` (a 400). Pass the `id` from the
222
+ * {@link ToolCallRequest} the model asked for. Ignored by providers that model
223
+ * tool results differently.
224
+ */
225
+ toolCallId?: string;
226
+ /**
227
+ * For `role: "assistant"`: the tool calls the model requested on that turn.
228
+ *
229
+ * Send it back with the assistant turn when you continue a tool conversation,
230
+ * so the following `role: "tool"` messages are valid. These are the same
231
+ * {@link ToolCallRequest}s that came back in {@link SavingsResult.toolCalls}.
232
+ */
233
+ toolCalls?: ToolCallRequest[];
210
234
  /** Opaque provider metadata; never used for the exact-cache key. */
211
235
  metadata?: Record<string, unknown>;
212
236
  }
@@ -1375,6 +1399,10 @@ interface SemanticCacheOptions {
1375
1399
  now?: () => number;
1376
1400
  }
1377
1401
  declare function cosine(a: number[], b: number[]): number;
1402
+ declare function numericTokens(s: string): string[];
1403
+ declare function multisetEqual(a: string[], b: string[]): boolean;
1404
+ declare function words(s: string): string[];
1405
+ declare function negationCount(tokens: string[]): number;
1378
1406
  /** True when two normalized queries agree on the facts an embedding tends to blur. */
1379
1407
  declare function keyTokensMatch(a: string, b: string): boolean;
1380
1408
  /**
@@ -1971,6 +1999,11 @@ declare function shouldCompress(input: CompressionPolicyInput): {
1971
1999
  declare function withProtectedSpans(inner: Compressor): Compressor;
1972
2000
  /** No-op compressor: the safe default until a real compressor sidecar is wired in. */
1973
2001
  declare const noopCompressor: Compressor;
2002
+ /**
2003
+ * Did the compression keep the facts that must not change? Verifies the RESULT —
2004
+ * number/date multiset and negation parity — not the compressor's own claim.
2005
+ */
2006
+ declare function compresionFiel(original: string, comprimido: string): boolean;
1974
2007
 
1975
2008
  /** What the layer needs to know about a provider's prefix cache to price a decision. */
1976
2009
  interface CacheTier {
@@ -3204,6 +3237,6 @@ declare function createSavingsLayer(config: Omit<SavingsLayerConfig, "tokenizer"
3204
3237
  * constant to package.json so the two can never drift.
3205
3238
  */
3206
3239
 
3207
- declare const BV_SALA_VERSION = "0.4.3";
3240
+ declare const BV_SALA_VERSION = "0.4.5";
3208
3241
 
3209
- export { type AnthropicOptions, AnthropicProvider, BV_SALA_VERSION, type BivelioHandoff, CATALOGO, COMPACT_ROWS_INSTRUCTION, type CacheOutcome, type CacheTier, type CachedAnswer, type CanonicalRequest, type ChatMessage, type CompactRowsDecodeOptions, type CompactRowsOptions, type CompactVerification, type CompressionEconomics, type CompressionPolicyInput, type CompressionRequest, type CompressionResult, type Compressor, type ConstraintBackend, type ConstraintBackendId, type ConstraintPlan, type ConversationState, type CostBasis, type CostModel, DEFAULT_COST_MODEL, DEFAULT_MAX_STALENESS_SECONDS, type DeferredExecutionReport, type DeterministicResolver, EMBEDDED_PUBLIC_KEY, type EmbeddingFn, type EvidenceDoc, type ExactCache, ExactTokenizer, type ExecutionClass, ExecutionRouter, type FormatChoice, FormatSelector, type FormatSelectorOptions, type HandoffFact, HeuristicEstimator, InMemoryExactCache, InMemorySemanticCache, type JsonSchema, LicenseError, type LicenseGate, type LicenseKeyset, type LicenseStatus, type LicenseTokenPayload, type LocalClassifier, MODELOS_POR_CODIFICACION, MODELS_BY_ENCODING, type MemoryDelta, type MessageRole, MockProvider, type MockProviderOptions, type ModelCascadeMode, type ModelCascadeReport, type ModelCascadeSettings, type OpenAICompatibleOptions, OpenAICompatibleProvider, type OutputContract, PRECIOS_AL_DIA, type Policy, type ProtectedSpan, type Provider, type ProviderRequest, type ProviderResponse, type ProviderUsage, QUALITY_STATUSES, type QualityStatus, type RequestContext, type Reranker, type ResolvedDeps, type ResolvedGate, type ResolvedLicense, ResolverRegistry, type ResolverResult, type RetrievalOptions, type RetrievalOutcome, type RetrievalPlan, type Retriever, type RevocationListBody, type RevocationListVerification, type RevocationPollEvent, RevocationPoller, type RevocationPollerOptions, RevocationSnapshot, type RouteDecision, type SavingsLayer, type SavingsLayerConfig, SavingsLedger, type SavingsReport, type SavingsRequest, type SavingsResult, type SavingsToggles, SchemaRegistry, type SemanticCacheOptions, type SemanticHit, type SemanticRecord, type SemanticVerifier, type SerializationFormat, type Serializer, Singleflight, type TokenEstimator, TokenizerRegistry, type ToolCallRequest, type ToolDescriptor, type ToolRisk, type ToolSelection, ToolSelector, type ToolSelectorOptions, TraceCollector, type TraceEvent, UMBRAL_LOCAL, UsageEmitter, type UsageEmitterOptions, type ValidationError, type ValidationResult, adaptiveK, arithmeticResolver, buildExactKey, buildInflightKey, canonicalize, compactRowsSerializer, computeNextDelay, cosine, createCompactRowsSerializer, createDefaultResolvers, createHandoff, createMockProvider, createSavingsLayer, csvSerializer, decodeCompactRows, dedupe, defaultTokenizerRegistry, deriveSeatId, deriveStatus, detectCi, embebedorLocal, emptyState, encodeCompactRows, evalArithmetic, evaluatePolicy, executeRetrievalPlan, gateFromKey, heuristicEstimator, isKeyset, isStrictCompatible, jsonCompactSerializer, keyTokensMatch, liveGateFromKey, mergeDelta, modelFamilyOf, noopCompressor, normalizeText, precioDeLista, projectContext, providerNativeBackend, randomId, registerExactTokenizer, registrarTokenizadorExacto, renderState, resolveConstraintBackend, resolveDependencies, resolveLicense, sha256, shouldCompress, stableStringify, uniformColumns, validateAgainstSchema, vectorLocal, verifyCompactSignature, verifyLicenseToken, verifyRevocationList, withProtectedSpans };
3242
+ export { type AnthropicOptions, AnthropicProvider, BV_SALA_VERSION, type BivelioHandoff, CATALOGO, COMPACT_ROWS_INSTRUCTION, type CacheOutcome, type CacheTier, type CachedAnswer, type CanonicalRequest, type ChatMessage, type CompactRowsDecodeOptions, type CompactRowsOptions, type CompactVerification, type CompressionEconomics, type CompressionPolicyInput, type CompressionRequest, type CompressionResult, type Compressor, type ConstraintBackend, type ConstraintBackendId, type ConstraintPlan, type ConversationState, type CostBasis, type CostModel, DEFAULT_COST_MODEL, DEFAULT_MAX_STALENESS_SECONDS, type DeferredExecutionReport, type DeterministicResolver, EMBEDDED_PUBLIC_KEY, type EmbeddingFn, type EvidenceDoc, type ExactCache, ExactTokenizer, type ExecutionClass, ExecutionRouter, type FormatChoice, FormatSelector, type FormatSelectorOptions, type HandoffFact, HeuristicEstimator, InMemoryExactCache, InMemorySemanticCache, type JsonSchema, LicenseError, type LicenseGate, type LicenseKeyset, type LicenseStatus, type LicenseTokenPayload, type LocalClassifier, MODELOS_POR_CODIFICACION, MODELS_BY_ENCODING, type MemoryDelta, type MessageRole, MockProvider, type MockProviderOptions, type ModelCascadeMode, type ModelCascadeReport, type ModelCascadeSettings, type OpenAICompatibleOptions, OpenAICompatibleProvider, type OutputContract, PRECIOS_AL_DIA, type Policy, type ProtectedSpan, type Provider, type ProviderRequest, type ProviderResponse, type ProviderUsage, QUALITY_STATUSES, type QualityStatus, type RequestContext, type Reranker, type ResolvedDeps, type ResolvedGate, type ResolvedLicense, ResolverRegistry, type ResolverResult, type RetrievalOptions, type RetrievalOutcome, type RetrievalPlan, type Retriever, type RevocationListBody, type RevocationListVerification, type RevocationPollEvent, RevocationPoller, type RevocationPollerOptions, RevocationSnapshot, type RouteDecision, type SavingsLayer, type SavingsLayerConfig, SavingsLedger, type SavingsReport, type SavingsRequest, type SavingsResult, type SavingsToggles, SchemaRegistry, type SemanticCacheOptions, type SemanticHit, type SemanticRecord, type SemanticVerifier, type SerializationFormat, type Serializer, Singleflight, type TokenEstimator, TokenizerRegistry, type ToolCallRequest, type ToolDescriptor, type ToolRisk, type ToolSelection, ToolSelector, type ToolSelectorOptions, TraceCollector, type TraceEvent, UMBRAL_LOCAL, UsageEmitter, type UsageEmitterOptions, type ValidationError, type ValidationResult, adaptiveK, arithmeticResolver, buildExactKey, buildInflightKey, canonicalize, compactRowsSerializer, compresionFiel, computeNextDelay, cosine, createCompactRowsSerializer, createDefaultResolvers, createHandoff, createMockProvider, createSavingsLayer, csvSerializer, decodeCompactRows, dedupe, defaultTokenizerRegistry, deriveSeatId, deriveStatus, detectCi, embebedorLocal, emptyState, encodeCompactRows, evalArithmetic, evaluatePolicy, executeRetrievalPlan, gateFromKey, heuristicEstimator, isKeyset, isStrictCompatible, jsonCompactSerializer, keyTokensMatch, liveGateFromKey, mergeDelta, modelFamilyOf, multisetEqual, negationCount, noopCompressor, normalizeText, numericTokens, precioDeLista, projectContext, providerNativeBackend, randomId, registerExactTokenizer, registrarTokenizadorExacto, renderState, resolveConstraintBackend, resolveDependencies, resolveLicense, sha256, shouldCompress, stableStringify, uniformColumns, validateAgainstSchema, vectorLocal, verifyCompactSignature, verifyLicenseToken, verifyRevocationList, withProtectedSpans, words };