@bivelio/savings-layer 0.4.4 → 0.4.6

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,31 +1,31 @@
1
1
  #!/usr/bin/env node
2
- import {spawn}from'child_process';import {createServer}from'http';import {randomUUID,createHash}from'crypto';var se=new Set;function y(t,e){se.has(t)||(se.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=
3
- 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.
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 w(e,t){be.has(e)||(be.add(e),console.warn(t));}var I=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
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(e){this.timer&&(clearInterval(this.timer),this.timer=void 0);let n=e??
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 e=this.options.fetchImpl??globalThis.
8
- 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:`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?(y(`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(e){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} (${e}). 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(e){this.buffer=[...e,...this.buffer],this.buffer.
16
- 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,
17
- 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,
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,o=0;for(let r=0;r<n&&this.buffer.length>0;r++){let s=this.buffer.length;if(await this.flush(),o=this.buffer.length>=s?o+1:0,o>=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 o=n.map(s=>this.toWire(s)),r=await t(this.options.url,{method:"POST",headers:{"content-type":"application/json",authorization:`Bear\
9
+ er ${this.options.serviceAccountKey}`},body:JSON.stringify(o)});r.ok?this.fallosSeguidos=0:r.status>=500?(this.fallosSeguidos++,this.requeue(n),this.avisarDeFallo(`el servidor respondi\xF3 ${r.status}`)):
10
+ r.status===401||r.status===403?(w(`metering-auth-${r.status}`,`[bv-sala] el env\xEDo de uso est\xE1 siendo rechazado con HTTP ${r.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?.(r.status)):
12
+ (this.fallosSeguidos++,this.avisarDeFallo(r.status===404||r.status===405?`HTTP ${r.status}: la URL de env\xEDo parece equivocada`:`el servidor rechaz\xF3 el lote con HTTP ${r.status}`));}catch(o){this.
13
+ fallosSeguidos++,this.requeue(n),this.avisarDeFallo(o instanceof Error?o.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
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 O(){return globalThis.process}function we(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]";
20
- return new RegExp(`-${e}{8,10}-${e}{5}$`).test(t)}function Se(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??
21
- t.NETLIFY)}async function Te(){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 Ce(t,e=Te){
22
- if(t.KUBERNETES_SERVICE_HOST||Se(t))return;let n=t.HOSTNAME??t.COMPUTERNAME??await e();if(!(!n||we(n)))return n}async function Re(){let t=O();if(!t||!t.env)return;let e=t.env,n=await Ce(e),r=[t.platform,
23
- 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 Ee=["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 D(){let t=O()?.env;if(!t)
25
- return false;for(let e of Ee){let n=t[e];if(n&&n!=="false"&&n!=="0")return true}return false}async function Ie(t,e){let n=globalThis.crypto,r=new TextEncoder;if(n?.subtle)try{let i=await n.subtle.importKey("raw",
26
- r.encode(t),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),c=await n.subtle.sign("HMAC",i,r.encode(e));return [...new Uint8Array(c)].map(u=>u.toString(16).padStart(2,"0")).join("")}catch{}let o=2166136261,s=`${t}\
27
- |${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&&Pe.test(n))return n;let r=await Re();
28
- if(!r)return;let o=await Ie(t,r);if(o)return `m-${o.slice(0,32)}`}var Pe=/^[A-Za-z0-9._:/-]{1,128}$/;var U={"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},
19
+ cache.result,baselineCost:n.cost.baselineEstimate,actualCost:n.cost.actual,netSavings:n.cost.netSavings,...n.cost.basis?{basis:n.cost.basis}:{}}}};function B(){return globalThis.process}function Ue(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 qe(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 Ke(){if(B()?.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 He(e,t=Ke){
22
+ if(e.KUBERNETES_SERVICE_HOST||qe(e))return;let n=e.HOSTNAME??e.COMPUTERNAME??await t();if(!(!n||Ue(n)))return n}async function Je(){let e=B();if(!e||!e.env)return;let t=e.env,n=await He(t),o=[e.platform,
23
+ e.arch,e.execPath,n,t.USER??t.USERNAME??t.LOGNAME,t.HOME??t.USERPROFILE].filter(r=>typeof r=="string"&&r.length>0);return o.length?o.join("|"):void 0}var We=["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 V(){let e=B()?.env;if(!e)
25
+ return false;for(let t of We){let n=e[t];if(n&&n!=="false"&&n!=="0")return true}return false}async function Ge(e,t){let n=globalThis.crypto,o=new TextEncoder;if(n?.subtle)try{let i=await n.subtle.importKey("raw",
26
+ o.encode(e),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),a=await n.subtle.sign("HMAC",i,o.encode(t));return [...new Uint8Array(a)].map(l=>l.toString(16).padStart(2,"0")).join("")}catch{}let r=2166136261,s=`${e}\
27
+ |${t}`;for(let i=0;i<s.length;i++)r^=s.charCodeAt(i),r=Math.imul(r,16777619);return (r>>>0).toString(16).padStart(8,"0")}async function F(e){let n=B()?.env?.BVSALA_SEAT_ID;if(n&&Ye.test(n))return n;let o=await Je();
28
+ if(!o)return;let r=await Ge(e,o);if(r)return `m-${r.slice(0,32)}`}var Ye=/^[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
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
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
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},
@@ -41,68 +41,135 @@ able-5":{inputPer1k:.01,outputPer1k:.05,cachedInputPer1k:.001},"claude-sonnet-4"
41
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
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
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 ie(t,e,n){if(!n)return;let r=t[n];if(r)
45
- return r;let o=n.lastIndexOf("/"),s=o>=0?n.slice(o+1):n,i=t[s];if(i)return i;for(let c of e){if(!s.startsWith(c))continue;let u=s.charAt(c.length);if(u!==""&&u!=="-"&&u!=="."&&u!=="_")continue;let a=s.
46
- slice(c.length+1);if(!(/^\d/.test(a)&&!/^\d{4}/.test(a)))return t[c]}}function q(t){return Object.keys(t).sort((e,n)=>n.length-e.length)}var _e=q(U);function I(t){return ie(U,_e,t)}var P=class{resto="";uso=null;eventos={};observar(e){this.resto+=typeof e=="string"?e:e.toString("utf8");let n;for(;(n=this.resto.indexOf(`
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 o=e[n];if(o)
45
+ return o;let r=n.lastIndexOf("/"),s=r>=0?n.slice(r+1):n,i=e[s];if(i)return i;for(let a of t){if(!s.startsWith(a))continue;let l=s.charAt(a.length);if(l!==""&&l!=="-"&&l!=="."&&l!=="_")continue;let p=s.
46
+ slice(a.length+1);if(!(/^\d/.test(p)&&!/^\d{4}/.test(p)))return e[a]}}function X(e){return Object.keys(e).sort((t,n)=>n.length-t.length)}var Qe=X(Q);function M(e){return ve(Q,Qe,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(`
47
47
 
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),c=i.usage??i.message?.usage;if(c){let u=c.cache_creation||this.uso?.cache_creation?{...this.uso?.cache_creation,...c.cache_creation}:void 0;
50
- this.uso={...this.uso,...c,...u?{cache_creation:u}:{}};}}catch{}}}usage(){return this.uso}},Oe=1.25,De=2;function ae(t,e){let n=I(t);if(!n)return null;let r=e.input_tokens??0,o=e.output_tokens??0,s=e.cache_read_input_tokens??
51
- 0,i=e.cache_creation,c=e.cache_creation_input_tokens??(i?.ephemeral_5m_input_tokens??0)+(i?.ephemeral_1h_input_tokens??0),u=Math.min(i?.ephemeral_1h_input_tokens??0,c),a=n.cachedInputPer1k??n.inputPer1k;
52
- return {costeUsd:r/1e3*n.inputPer1k+s/1e3*a+(c-u)/1e3*n.inputPer1k*Oe+u/1e3*n.inputPer1k*De+o/1e3*n.outputPer1k,precio:n,tokens:{entrada:r,salida:o,cacheLeida:s,cacheEscrita:c,cacheEscrita1h:u}}}function K(t){
53
- try{return JSON.parse(typeof t=="string"?t:t.toString("utf8")).usage??null}catch{return null}}function ce(t,e,n){return ue(t,e,n,false)}function le(t,e,n){return ue(t,e,n,true)}function ue(t,e,n,r){let o=n.
54
- 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,
55
- 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,
56
- actual:0,netSavings:1,basis:"estimated"}:{baselineEstimate:n.costeUsd,actual:n.costeUsd,netSavings:0,basis:"measured"}}}var J=5*1024*1024,Me=["authorization","x-api-key","anthropic-version","anthropic-beta"];function de(t,e,n,r=""){return createHash("sha256").update(t).update(`
57
- `).update(e).update(`
58
- `).update(r).update(`
59
- `).update(n).digest("hex")}function pe(t){return Me.map(e=>`${e}=${t(e)??""}`).join(`
60
- `)}var M=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(c=>n=c);this.enVuelo.set(e,{promesa:r,resolver:n});let o=[],s=0,
61
- i=false;return {capturar:c=>{if(!i){if(s+=c.length,s>J){i=true,o.length=0;return}o.push(c);}},terminar:(c,u)=>{let a=!i&&c>=200&&c<300;this.enVuelo.get(e)?.resolver(a?{status:c,headers:u,chunks:o}:null),this.
62
- 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+=
63
- 1),r}tieneEnVuelo(e){return this.enVuelo.has(e)}};var je=["content-encoding","content-length","transfer-encoding"];async function me(t={}){let e=(t.upstream??"https://api.anthropic.com").replace(/\/+$/,""),n=t.fetchImpl??fetch,r=t.log??(a=>console.log(
64
- 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:l=>y(
65
- "gateway-ingest-auth",`[bvsala gateway] la ingesta de uso rechaza la credencial (${l}). El tr\xE1fico sigue fluyendo (avisar, nunca romper), pero este uso no est\xE1 llegando al panel. Revisa BIVELIO_SERVIC\
66
- E_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(l=>o?.setSeat(l,a)).catch(()=>{}),o.start();}else y("gateway-sin-\
67
- 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.
68
- coalescer?new M:null,i=(a,l,m,h,b)=>{if(!o||!m)return;let d="";try{d=JSON.parse(l.toString("utf8")).model??"";}catch{}let p=d?ae(d,m):null;p?(o.record(h?le(a,d,p):ce(a,d,p)),r(`[gateway] POST ${b} ${h?
69
- "\xB7 COALESCIDA (llamada evitada)":""} \xB7 ${d} \xB7 $${p.costeUsd.toFixed(6)} (in ${p.tokens.entrada}, cache\u2191${p.tokens.cacheEscrita}${p.tokens.cacheEscrita1h?` (1h ${p.tokens.cacheEscrita1h})`:
70
- ""} cache\u2193${p.tokens.cacheLeida}, out ${p.tokens.salida})`)):d&&y(`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\
71
- .`);},c=createServer(async(a,l)=>{let m=randomUUID(),h=new AbortController;l.on("close",()=>{l.writableEnded||h.abort();});let b=null,d=new URL(a.url??"/",e);try{let p=[];for await(let f of a)p.push(f);let k=Buffer.concat(
72
- p);d=new URL(a.url??"/",e);let C=s!==null&&a.method==="POST"&&k.length>0,z=pe(f=>{let g=a.headers[f];return Array.isArray(g)?g.join(", "):g}),R=C?de("POST",d.pathname+d.search,k,z):"";if(C)if(s.tieneEnVuelo(
73
- R)){let f=await s.esperar(R);if(f){l.writeHead(f.status,f.headers);let g=new P;for(let re of f.chunks)l.write(re),g.observar(re);l.end();let xe=g.usage()??(d.pathname==="/v1/messages"?K(Buffer.concat(
74
- f.chunks)):null);i(m,k,xe,!0,d.pathname);return}}else b=s.registrar(R);let ne={};for(let[f,g]of Object.entries(a.headers))f==="host"||f==="content-length"||g===void 0||(ne[f]=Array.isArray(g)?g.join("\
75
- , "):g);let v;try{v=await n(d,{method:a.method,headers:ne,body:["GET","HEAD"].includes(a.method??"")?void 0:k,redirect:"manual",signal:h.signal});}catch(f){b?.abortar(),l.headersSent||(l.writeHead(502,
76
- {"content-type":"application/json"}),l.end(JSON.stringify({error:"bvsala gateway: upstream unreachable",detail:String(f)}))),r(`[gateway] ${a.method} ${d.pathname} \u2192 upstream inalcanzable: ${String(
77
- f)}`);return}let B=Object.fromEntries(v.headers.entries());for(let f of je)delete B[f];l.writeHead(v.status,B);let F=(v.headers.get("content-type")??"").includes("text/event-stream"),V=F?new P:null,E=!F&&
78
- v.ok&&o!==void 0&&a.method==="POST"&&d.pathname==="/v1/messages"&&(v.headers.get("content-type")??"").includes("application/json")?[]:null,oe=0;if(v.body)for await(let f of v.body){let g=Buffer.from(f);
79
- l.write(g),V?.observar(g),E&&(oe+=g.length,oe>J?E=null:E.push(g)),b?.capturar(g);}l.end(),b?.terminar(v.status,B),V&&v.ok?i(m,k,V.usage(),!1,d.pathname):E?i(m,k,K(Buffer.concat(E)),!1,d.pathname):F||r(
80
- `[gateway] ${a.method} ${d.pathname} \u2192 ${v.status}`);}catch(p){if(b?.abortar(),h.signal.aborted)r(`[gateway] ${a.method} ${d.pathname} \u2192 el cliente abort\xF3; upstream cancelado`);else if(r(`\
81
- [gateway] ${a.method} ${d.pathname} \u2192 error sirviendo: ${String(p)}`),!l.headersSent){l.writeHead(502,{"content-type":"application/json"}),l.end(JSON.stringify({error:"bvsala gateway: stream inte\
82
- rrupted",detail:String(p)}));return}l.writableEnded||l.end();}});return await new Promise((a,l)=>{c.once("error",l),c.listen(t.puerto??8402,"127.0.0.1",()=>a());}),{puerto:c.address().port,cerrar:async()=>{
83
- await o?.stopAndDrain(),await new Promise(a=>c.close(()=>a()));}}}var H=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.
84
- 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,
85
- Math.ceil(e.length/4))}},W=new H;var G=class{estimators=new Map;modelToTokenizer=new Map;fallbackId;constructor(e=W){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.
86
- 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);
87
- if(o)return o}}return this.estimators.get(this.fallbackId)}count(e,n){return this.resolve(n).count(e)}};new G;new TextEncoder;new TextDecoder;new TextDecoder;var Ze={"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}};
88
- q(Ze);var Z="0.4.4";var[,,x,...N]=process.argv;function tt(){console.log(`bvsala ${Z} \u2014 BiVelio Savings Layer
48
+ `))>=0;){let o=this.resto.slice(0,n);this.resto=this.resto.slice(n+2);let r=/^event: (.+)$/m.exec(o)?.[1];if(r&&(this.eventos[r]=(this.eventos[r]??0)+1),r!=="message_start"&&r!=="message_delta")continue;
49
+ let s=/^data: (.+)$/m.exec(o)?.[1];if(s)try{let i=JSON.parse(s),a=i.usage??i.message?.usage;if(a){let l=a.cache_creation||this.uso?.cache_creation?{...this.uso?.cache_creation,...a.cache_creation}:void 0;
50
+ this.uso={...this.uso,...a,...l?{cache_creation:l}:{}};}}catch{}}}usage(){return this.uso}usageFinal(){return (this.eventos.message_delta??0)>0?this.uso:null}},U=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 o=this.resto.slice(0,n);this.resto=this.resto.slice(n+2);let r=/^event: (.+)$/m.exec(o)?.[1];r&&(this.eventos[r]=(this.eventos[r]??0)+1);let s=/^data: (.+)$/m.exec(o)?.[1];if(s)try{let i=JSON.
54
+ parse(s);if((r??i.type)!=="response.completed")continue;let a=i.response?.usage;a&&(this.uso=Xe(a));}catch{}}}usage(){return this.uso}usageFinal(){return this.uso}};function Xe(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),o=Math.min(Math.max(0,e.input_tokens_details?.cache_write_tokens??0),t-n);return {input_tokens:t-n-o,output_tokens:Math.max(0,e.output_tokens??
56
+ 0),cache_read_input_tokens:n,cache_creation_input_tokens:o}}var ye=1.25,Ze=2;function ke(e,t){let n=M(e);if(!n)return null;let o=t.input_tokens??0,r=t.output_tokens??0,s=t.cache_read_input_tokens??0,i=t.
57
+ cache_creation,a=t.cache_creation_input_tokens??(i?.ephemeral_5m_input_tokens??0)+(i?.ephemeral_1h_input_tokens??0),l=Math.min(i?.ephemeral_1h_input_tokens??0,a),p=n.cachedInputPer1k??n.inputPer1k;return {
58
+ costeUsd:o/1e3*n.inputPer1k+s/1e3*p+(a-l)/1e3*n.inputPer1k*tt(e)+l/1e3*n.inputPer1k*Ze+r/1e3*n.outputPer1k,precio:n,tokens:{entrada:o,salida:r,cacheLeida:s,cacheEscrita:a,cacheEscrita1h:l}}}function xe(e,t){
59
+ try{let n=e,o={maxOutputLength:et};switch((t??"").trim().toLowerCase()){case "gzip":case "x-gzip":n=T.gunzipSync(e,o);break;case "deflate":n=T.inflateSync(e,o);break;case "br":n=T.brotliDecompressSync(e,o);
60
+ break;case "zstd":{let s=T.zstdDecompressSync;if(typeof s!="function")return "";n=s(e,o);break}default:break}let r=JSON.parse(n.toString("utf8")).model;return typeof r=="string"?r:""}catch{return ""}}var et=5*
61
+ 1024*1024;function we(){return typeof T.zstdDecompressSync=="function"}function tt(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 Se(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,o){let r=n.tokens.entrada+n.tokens.cacheLeida+n.tokens.cacheEscrita;return {requestId:e,model:t,calls:o?{avoidedLlm:1,actualLlm:0}:{avoidedLlm:0,actualLlm:1},tokens:{
64
+ baselineInput:r,actualInput:o?0:r,eliminatedInput:0,compressedInput:0,baselineOutput:n.tokens.salida,actualOutput:o?0:n.tokens.salida,providerCachedInput:o?0:n.tokens.cacheLeida},cache:{result:o?"sing\
65
+ leflight":"miss"},cost:o?{baselineEstimate:n.costeUsd,actual:0,netSavings:1,basis:"estimated"}:{baselineEstimate:n.costeUsd,actual:n.costeUsd,netSavings:0,basis:"measured"}}}var ee=5*1024*1024,ot=["authorization","x-api-key","anthropic-version","anthropic-beta"];function Re(e,t,n,o=""){return createHash("sha256").update(e).update(`
66
+ `).update(t).update(`
67
+ `).update(o).update(`
68
+ `).update(n).digest("hex")}function Ee(e){return ot.map(t=>`${t}=${e(t)??""}`).join(`
69
+ `)}var q=class{enVuelo=new Map;primarias=0;gemelas=0;registrar(t){if(this.enVuelo.has(t))return null;this.primarias+=1;let n,o=new Promise(a=>n=a);this.enVuelo.set(t,{promesa:o,resolver:n});let r=[],s=0,
70
+ i=false;return {capturar:a=>{if(!i){if(s+=a.length,s>ee){i=true,r.length=0;return}r.push(a);}},terminar:(a,l)=>{let p=!i&&a>=200&&a<300;this.enVuelo.get(t)?.resolver(p?{status:a,headers:l,chunks:r}: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 o=await n.promesa;return o&&(this.gemelas+=
72
+ 1),o}tieneEnVuelo(t){return this.enVuelo.has(t)}};var it=["content-encoding","content-length","transfer-encoding"];async function Ie(e={}){let t=(e.upstream??"https://api.anthropic.com").replace(/\/+$/,""),n=e.fetchImpl??fetch,o=e.log??(c=>console.log(
73
+ c)),r;if(e.serviceAccountKey){r=new I({url:e.meteringUrl??"https://savings.bivelio.com/api/metering/usage",serviceAccountKey:e.serviceAccountKey,...e.fetchImpl?{fetchImpl:e.fetchImpl}:{},onAuthError:u=>w(
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 c=V();e.seatId?r.setSeat(e.seatId,c):F(e.serviceAccountKey).then(u=>r?.setSeat(u,c)).catch(()=>{}),r.start();}else w("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 q:null,i=c=>c==="/v1/messages"?new $:c.endsWith("/responses")?new U:null,a=(c,u,f,b,m,d)=>{if(!r||!f)return;let h=xe(u,d);if(!h){w(`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 k=ke(h,f);k?(r.record(b?Te(c,h,k):Se(c,h,k)),o(`[gateway] POST ${m} ${b?"\xB7 COA\
79
+ LESCIDA (llamada evitada)":""} \xB7 ${h} \xB7 $${k.costeUsd.toFixed(6)} (in ${k.tokens.entrada}, cache\u2191${k.tokens.cacheEscrita}${k.tokens.cacheEscrita1h?` (1h ${k.tokens.cacheEscrita1h})`:""} cac\
80
+ he\u2193${k.tokens.cacheLeida}, out ${k.tokens.salida})`)):w(`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
+ l=createServer(async(c,u)=>{let f=randomUUID(),b=new AbortController;u.on("close",()=>{u.writableEnded||b.abort();});let m=null,d=new URL(c.url??"/",t),h=null,k=Buffer.alloc(0),N=false,O=c.headers["content-encoding"],z=Array.
82
+ isArray(O)?O[0]:O;try{let D=[];for await(let g of c)D.push(g);let S=Buffer.concat(D);k=S,d=new URL(c.url??"/",t);let pe=s!==null&&c.method==="POST"&&S.length>0,Ve=Ee(g=>{let v=c.headers[g];return Array.
83
+ isArray(v)?v.join(", "):v}),W=pe?Re("POST",d.pathname+d.search,S,Ve):"";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 Fe=v.usage()??(d.pathname==="/v1/messages"?Z(Buffer.concat(g.chunks)):null);a(f,S,Fe,!0,d.pathname,z);return}}else m=s.registrar(W);let me={};for(let[g,v]of Object.
85
+ entries(c.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:c.method,headers:me,body:["GET","HEAD"].includes(c.method??"")?void 0:
86
+ S,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)}))),o(`[gateway] ${c.method} ${d.pathname} \u2192 upstream inalcanzable: ${String(g)}`);return}let G=Object.fromEntries(x.headers.entries());for(let g of it)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&&r!==void 0&&c.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?(N=!0,a(f,S,h.usage(),!1,d.pathname,z)):L?a(f,S,Z(Buffer.concat(L)),!1,d.pathname,z):Y||o(`[gateway] ${c.method} ${d.pathname} \u2192 ${x.status}`);}catch(D){
91
+ if(m?.abortar(),b.signal.aborted){let S=!N&&h?h.usageFinal():null;S?(o(`[gateway] ${c.method} ${d.pathname} \u2192 el cliente cerr\xF3 tras el usage final; se mide igual`),a(f,k,S,false,d.pathname,z)):o(
92
+ `[gateway] ${c.method} ${d.pathname} \u2192 el cliente abort\xF3; upstream cancelado`);}else if(o(`[gateway] ${c.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 l.on("upgrade",(c,u)=>{o(`[gatew\
94
+ ay] ${c.method} ${new URL(c.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((c,u)=>{l.once("error",u),l.listen(e.puerto??8402,"127.0.0.1",()=>c());}),{puerto:l.address().port,cerrar:async()=>{await r?.stopAndDrain(),await new Promise(c=>l.close(()=>c()));}}}var te=["claude","codex","goose","qwen"];function _e(e){return te.includes(e??"")}function Ae(e,t={}){let n=t.path??process.env.PATH??"",o=t.
100
+ existe??existsSync,r=null;for(let s of n.split(";").filter(i=>i.length>0)){for(let i of [".exe",".com"]){let a=join(s,e+i);if(o(a))return {tipo:"exe",ruta:a}}if(r===null)for(let i of [".cmd",".bat"]){let a=join(s,e+
101
+ i);if(o(a)){r=a;break}}}return r!==null?{tipo:"solo-shim",ruta:r}:{tipo:"no-encontrado"}}function Oe(e={}){let n=(e.env??process.env).BVSALA_CODEX_AUTH?.toLowerCase();if(n==="api-key"||n==="apikey")return "\
102
+ api-key";if(n==="chatgpt")return "chatgpt";let o=null;try{o=e.leerAuth?e.leerAuth():null;}catch{o=null;}if(o===null)return "api-key";try{let r=JSON.parse(o);return typeof r.auth_mode=="string"?r.auth_mode.
103
+ toLowerCase().replace(/[^a-z]/g,"")==="apikey"?"api-key":"chatgpt":r.tokens!==void 0&&r.tokens!==null?"chatgpt":"api-key"}catch{return "api-key"}}var ct={"api-key":{upstream:"https://api.openai.com",prefijo:"\
104
+ /v1"},chatgpt:{upstream:"https://chatgpt.com",prefijo:"/backend-api/codex"}};function ne(e,t,n,o={}){let r=`http://127.0.0.1:${t}`;switch(e){case "claude":return {bin:"claude",args:n,env:{ANTHROPIC_BASE_URL:r}};case "\
105
+ goose":return {bin:"goose",args:n,env:{ANTHROPIC_HOST:r}};case "qwen":return {bin:"qwen",args:n,env:{ANTHROPIC_BASE_URL:r},nota:'Qwen Code solo enruta por aqu\xED con `security.auth.selectedType: "anthropic\
106
+ "` en ~/.qwen/settings.json (ver `bvsala doctor`).'};case "codex":{let s=o.modoCodex??"api-key",i=ct[s];return {bin:"codex",args:["-c",`openai_base_url="${r}${i.prefijo}"`,...n],env:{},upstream:i.upstream,
107
+ 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\
108
+ tokens.":"Auth por API key: el gateway enruta a api.openai.com/v1."}}}}var pt="comprobar: en el log del gateway debe aparecer `[gateway] POST /v1/messages \u2026`; si ve\
109
+ s `/v1/v1/messages`, sobra un /v1; si ves `POST /messages` a secas, falta uno.",mt=[{id:"claude",nombre:"Claude Code",binario:"claude",configs:[],detectable:true,receta:({base:e})=>["usa `bvsala claude`\
110
+ \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\
111
+ 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 \
112
+ 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\
113
+ 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,
114
+ rutas:t})=>[`mecanismo: ${t[0]} \u2192 provider.anthropic.options.baseURL (el opencode.json del proyecto pisa al global). CON /v1.`,"{",' "provider": {',' "anthropic": {',` "options": { "base\
115
+ 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",
116
+ 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\
117
+ 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\
118
+ 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:"\
119
+ 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\
120
+ 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:"\
121
+ 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\
122
+ idar en mano contra el log del gateway].`,"{",' "customModels": ['," {",' "model": "<id de modelo Anthropic>",',' "displayName": "<nombre visible>",',' "provider": "anthropic",',` \
123
+ "baseUrl": "${e}",`,' "apiKey": "<tu API key de Anthropic>"'," }"," ]","}"]},{id:"cline",nombre:"Cline",configs:[],detectable:false,receta:({baseV1:e})=>["mecanismo: UI \u2192 proveedor Anthropic\
124
+ \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,
125
+ 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 ft(e,t){
126
+ let n=e.split(delimiter).filter(r=>r.length>0),o=process.platform==="win32"?["",".exe",".cmd",".bat"]:[""];return r=>n.some(s=>o.some(i=>t(join(s,r+i))))}async function De(e,t={}){if(!Number.isInteger(e)||e<1||
127
+ e>65535)throw new RangeError(`[bvsala doctor] puerto inv\xE1lido: ${String(e)}`);let n=t.home??homedir(),o=t.existe??existsSync,r=t.enPath??ft(t.path??process.env.PATH??"",o),s=`http://127.0.0.1:${e}`,i=`${s}/v1`,
128
+ a=[],l=[`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 \
129
+ se lee el contenido de ning\xFAn archivo.",""];for(let p of mt){let c=p.configs.map(d=>join(n,d)),u=p.binario!==void 0&&r(p.binario),f=c.filter(d=>o(d)),b=u||f.length>0;b&&a.push(p.id);let m;p.detectable?
130
+ 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)",l.
131
+ push(`\xBB ${p.id}: ${m}`),l.push(` ${p.nombre}`);for(let d of p.receta({base:s,baseV1:i,rutas:c}))l.push(` ${d}`);l.push(` ${pt}`),l.push("");}return l.push("Regla BYOK: las recetas del anillo Anth\
132
+ 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 \
133
+ 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:l.join(`
134
+ `),detectados:a}}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)??[],o=0;for(let r of n){let s=r[0];if(/\s/.test(s)){let i=(r.
135
+ match(/\n/g)??[]).length;o+=i+Math.floor(Math.max(0,r.length-1)/2);}else /[A-Za-z]/.test(s)?o+=Math.max(1,Math.ceil(r.length/4)):/\d/.test(s)?o+=Math.max(1,Math.ceil(r.length/2)):o+=1;}return Math.max(o,
136
+ 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){
137
+ 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 o=this.modelToTokenizer.get(t.model);if(o){let r=this.estimators.
138
+ get(o);if(r)return r}}return this.estimators.get(this.fallbackId)}count(t,n){return this.resolve(n).count(t)}};new se;new TextEncoder;new TextDecoder;new TextDecoder;var _t={"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}};
139
+ X(_t);var le="0.4.6";var[,,y,...A]=process.argv;function Mt(){console.log(`bvsala ${le} \u2014 BiVelio Savings Layer
89
140
 
90
141
  Comandos:
91
- gateway [--puerto N] Proxy local medidor para Claude Code (ANTHROPIC_BASE_URL).
142
+ gateway [--puerto N] Proxy local medidor (por defecto 8402) para terminales que
143
+ respetan un base URL: Claude Code, opencode, Goose, Crush\u2026
92
144
  claude [args\u2026] Lanza Claude Code a trav\xE9s del gateway, sin configurar nada.
145
+ codex [args\u2026] Lanza Codex CLI a trav\xE9s del gateway (API key: api.openai.com;
146
+ auth ChatGPT: chatgpt.com/backend-api/codex \u2014 se mide, no se monetiza).
147
+ goose [args\u2026] Lanza Goose (ANTHROPIC_HOST) a trav\xE9s del gateway.
148
+ qwen [args\u2026] Lanza Qwen Code (ANTHROPIC_BASE_URL) a trav\xE9s del gateway.
149
+ doctor [--puerto N] Detecta terminales instalados e imprime su configuraci\xF3n exacta.
93
150
 
94
151
  Entorno:
95
152
  BIVELIO_SERVICE_KEY Clave de servicio (bvk_\u2026): habilita metering + asiento.
96
153
  BVSALA_SEAT_ID Identidad de asiento estable por r\xE9plica (opcional).
97
- BVSALA_GATEWAY_UPSTREAM Upstream alternativo (por defecto https://api.anthropic.com).
154
+ BVSALA_GATEWAY_UPSTREAM Upstream alternativo (por defecto el del terminal).
98
155
  BVSALA_GATEWAY_COALESCE =1 enciende la coalescencia de duplicados (F2, experimental).
156
+ BVSALA_CODEX_AUTH api-key | chatgpt \u2014 fuerza el modo de auth de Codex.
157
+ CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 (Claude Code, solo API key) tus modelos en /model.
99
158
 
100
159
  El gateway nunca corta el tr\xE1fico: sin clave o con la ingesta ca\xEDda, reenv\xEDa y avisa.
101
- Tus prompts jam\xE1s salen de tu m\xE1quina \u2014 solo cuentas y coste. M\xE1s: savings.bivelio.com`);}x!=="gateway"&&x!=="claude"&&(tt(),process.exit(x===void 0||x==="help"||x==="--help"?0:1));var ee=N.
102
- indexOf("--puerto"),j=x==="claude"?0:ee>=0?Number(N[ee+1]):8402;(!Number.isInteger(j)||j<0||j>65535)&&(console.error(`[bvsala gateway] puerto inv\xE1lido: ${N[ee+1]}`),process.exit(1));var te=process.
103
- env.BIVELIO_SERVICE_KEY,T=await me({puerto:j,...te?{serviceAccountKey:te}:{},...process.env.BVSALA_SEAT_ID?{seatId:process.env.BVSALA_SEAT_ID}:{},...process.env.BVSALA_GATEWAY_UPSTREAM?{upstream:process.
104
- 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(`[bvsal\
105
- a gateway] v${Z} escuchando en http://127.0.0.1:${T.puerto}`);console.log(`[bvsala gateway] medici\xF3n: ${te?"ACTIVA (clave de servicio presente)":"APAGADA (falta BIVELIO_SERVICE_KEY)"}`);if(x==="cla\
106
- ude"){let t=spawn("claude",N,{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(
107
- 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 her\
108
- ramienta: ANTHROPIC_BASE_URL=http://127.0.0.1:${T.puerto} claude`);var ye=async()=>{await T.cerrar(),process.exit(0);};process.on("SIGINT",()=>{ye();});process.on("SIGTERM",()=>{ye();});
160
+ Tus prompts jam\xE1s salen de tu m\xE1quina \u2014 solo cuentas y coste. M\xE1s: savings.bivelio.com`);}var J=_e(y);y!=="gateway"&&y!=="doctor"&&!J&&(Mt(),process.exit(y===void 0||y==="help"||y==="--he\
161
+ lp"?0:1));var ue=A.indexOf("--puerto"),_=J?0:ue>=0?Number(A[ue+1]):8402;(!Number.isInteger(_)||(y==="doctor"?_<1:_<0)||_>65535)&&(console.error(`[bvsala ${y}] puerto inv\xE1lido: ${A[ue+1]}`),process.
162
+ exit(1));if(y==="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 De(_);console.
163
+ log(e.texto),process.exit(0);}var R=J?ne(y,0,A,{modoCodex:y==="codex"?Oe({leerAuth:()=>readFileSync(join(homedir(),".codex","auth.json"),"utf8")}):void 0}):null,de=process.env.BIVELIO_SERVICE_KEY,E=await Ie({puerto:_,...de?
164
+ {serviceAccountKey:de}:{},...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.
165
+ upstream}:{},...process.env.BVSALA_GATEWAY_COALESCE==="1"?{coalescer:true}:{},...process.env.BVSALA_METERING_URL?{meteringUrl:process.env.BVSALA_METERING_URL}:{}}),$t=process.env.BVSALA_GATEWAY_UPSTREAM??
166
+ R?.upstream??"https://api.anthropic.com";console.log(`[bvsala gateway] v${le} escuchando en http://127.0.0.1:${E.puerto} \u2192 ${$t}`);console.log(`[bvsala gateway] medici\xF3n: ${de?"ACTIVA (clave d\
167
+ e servicio presente)":"APAGADA (falta BIVELIO_SERVICE_KEY)"}`);process.env.BVSALA_GATEWAY_UPSTREAM&&R?.upstream&&process.env.BVSALA_GATEWAY_UPSTREAM!==R.upstream&&console.error(`[bvsala ${y}] AVISO: B\
168
+ VSALA_GATEWAY_UPSTREAM=${process.env.BVSALA_GATEWAY_UPSTREAM} pisa el upstream de este terminal (${R.upstream}). Si no es lo que quieres, quita la variable.`);y==="codex"&&!we()&&console.error(`[bvsal\
169
+ a codex] AVISO: este Node (${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&&J){let e=ne(y,E.puerto,A,{...R.upstream?
170
+ {modoCodex:R.upstream.includes("chatgpt.com")?"chatgpt":"api-key"}:{}});e.nota&&console.log(`[bvsala ${y}] ${e.nota}`);let t=e.bin;if(process.platform==="win32"){let o=Ae(e.bin);o.tipo==="exe"?t=o.ruta:
171
+ o.tipo==="solo-shim"&&(console.error(`[bvsala ${y}] \`${e.bin}\` est\xE1 instalado como shim de npm (${o.ruta}) y Windows no puede lanzarlo de forma segura con argumentos (CVE-2024-27980). Inst\xE1lalo c\
172
+ on su instalador nativo (deja un ${e.bin}.exe en el PATH) o usa la receta manual de docs/terminales/.`),await E.cerrar(),process.exit(127));}let n=spawn(t,e.args,{stdio:"inherit",env:{...process.env,...e.
173
+ env}});n.on("error",o=>{console.error(`[bvsala ${y}] no se pudo lanzar \`${e.bin}\`: ${String(o)}. \xBFEst\xE1 instalado y en el PATH?`),E.cerrar().then(()=>process.exit(127));}),n.on("exit",(o,r)=>{E.
174
+ cerrar().then(()=>process.exit(o??(r?1:0)));});}else console.log(`[bvsala gateway] apunta tu terminal (o usa \`bvsala ${te.join("|")}\`): ANTHROPIC_BASE_URL=http://127.0.0.1:${E.puerto} claude \xB7 bv\
175
+ sala doctor para el resto`);var Be=async()=>{await E.cerrar(),process.exit(0);};process.on("SIGINT",()=>{Be();});process.on("SIGTERM",()=>{Be();});
package/dist/index.d.ts CHANGED
@@ -3237,6 +3237,6 @@ declare function createSavingsLayer(config: Omit<SavingsLayerConfig, "tokenizer"
3237
3237
  * constant to package.json so the two can never drift.
3238
3238
  */
3239
3239
 
3240
- declare const BV_SALA_VERSION = "0.4.4";
3240
+ declare const BV_SALA_VERSION = "0.4.6";
3241
3241
 
3242
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 };
package/dist/index.js CHANGED
@@ -781,7 +781,7 @@ ng/usage`:void 0);if(a){i=new dt({url:a,serviceAccountKey:t.serviceAccountKey,..
781
781
  {},...t.metering?.maxBatch!=null?{maxBatch:t.metering.maxBatch}:{},...t.metering?.maxBuffer!=null?{maxBuffer:t.metering.maxBuffer}:{},...t.meteringFetch?{fetchImpl:t.meteringFetch}:t.licenseFetch?{fetchImpl:t.
782
782
  licenseFetch}:{},...t.now?{now:t.now}:{},...t.metering?.ci!=null?{ci:t.metering.ci}:{},...t.silenceWarnings!=null?{silenceWarnings:t.silenceWarnings}:{}});let c=t.metering?.ci??cr();if(t.metering?.seatId)
783
783
  i.setSeat(t.metering.seatId,c);else {let l=i;lr(t.serviceAccountKey).then(u=>l.setSeat(u,c)).catch(()=>{});}i.start();}}return {generate:async a=>{let c=await s.generate(a);if(i)try{i.record(c.report);}catch{}
784
- return c},close:()=>{r?.stop(),o?.stop(),i?.stop();}}}var af="0.4.4";/*! Bundled license information:
784
+ return c},close:()=>{r?.stop(),o?.stop(),i?.stop();}}}var af="0.4.6";/*! Bundled license information:
785
785
 
786
786
  @noble/curves/utils.js:
787
787
  @noble/curves/abstract/modular.js:
@@ -0,0 +1,133 @@
1
+ # Guías por terminal — medir el gasto de IA con el gateway local
2
+
3
+ **Fecha:** 2026-08-30 · **Fuente de verdad:** `docs/PASARELA-S3-TERMINALES.md`
4
+ §3 (formas de URL y mecanismos de enrutado verificados contra el código de cada
5
+ terminal). Cada afirmación va marcada **[V]** (verificada en fuente) o **[I]**
6
+ (inferida). Si una guía contradice a ese documento, manda el documento.
7
+
8
+ ## Qué es esto
9
+
10
+ `bvsala gateway` es un proxy local medidor: escucha en `127.0.0.1`, reenvía
11
+ cada petición **byte a byte** al proveedor (`https://api.anthropic.com` por
12
+ defecto) y, una vez servida la respuesta, lee el `usage` del stream y lo
13
+ convierte en coste. Tus prompts jamás salen de tu máquina hacia BiVelio — solo
14
+ cuentas y coste. Nunca corta el tráfico: sin clave o con la ingesta caída,
15
+ reenvía y avisa.
16
+
17
+ Hoy **mide**; no optimiza todavía. Y mide **todo el tráfico de inferencia**
18
+ que el terminal enruta por el base URL — no «todo el tráfico»: cada guía dice
19
+ lo que su terminal manda por fuera.
20
+
21
+ ## Índice
22
+
23
+ | Guía | Base URL exacto | `/v1` | Enrutado [V] | Envoltorio | Estado |
24
+ |---|---|---|---|---|---|
25
+ | [Claude Code](claude-code.md) | `http://127.0.0.1:8402` | **sin** | env `ANTHROPIC_BASE_URL` | `bvsala claude` ✅ | [V] en producción |
26
+ | [Codex CLI](codex.md) | `http://127.0.0.1:8402/v1` (API key) · `…/backend-api/codex` (ChatGPT) | **con** | `codex -c openai_base_url=…` | `bvsala codex` ✅ | [V] validado en mano el 30-08 (426 → fallback HTTP, stream íntegro) |
27
+ | [Goose](goose.md) | `http://127.0.0.1:8402` | **sin** | env `ANTHROPIC_HOST` | `bvsala goose` ✅ | [V] |
28
+ | [Qwen Code](qwen-code.md) | `http://127.0.0.1:8402` | **sin** | env `ANTHROPIC_BASE_URL` + `~/.qwen/settings.json` | `bvsala qwen` ✅ | [V] |
29
+ | [opencode](opencode.md) | `http://127.0.0.1:8402/v1` | **con** | `~/.config/opencode/opencode.json` | — (config de usuario) | [V] |
30
+ | [Crush](crush.md) | `http://127.0.0.1:8402/v1` | **con**, sin barra final | `~/.config/crush/crushrc` | — (config de usuario) | [V] |
31
+ | [Droid](droid.md) | `http://127.0.0.1:8402` | sin **[I]** | `~/.factory/settings.json` | — (config de usuario) | validar en mano |
32
+ | [Cline](cline.md) | `http://127.0.0.1:8402/v1` | **con** | UI | — | [V] |
33
+ | [Kilo Code](kilo.md) | `http://127.0.0.1:8402/v1` | con **[I]** | UI | — | validar en mano |
34
+
35
+ Reglas de la columna «Base URL»: **nunca barra final**, **nunca redirecciones**
36
+ (un 3xx en el base URL es fallo por diseño del cliente, y el gateway lo reenvía
37
+ en vez de seguirlo). Y nunca copies el URL de un terminal a otro: cada SDK añade
38
+ su propio tramo de ruta y el error es silencioso (§ «Comprobar que mide»).
39
+
40
+ Claude Code, Codex, Goose y Qwen Code tienen **envoltorio propio** (`bvsala
41
+ claude|codex|goose|qwen`): arrancan un gateway en puerto efímero, lanzan el
42
+ terminal apuntándole y lo apagan al salir. Cero configuración persistente. La
43
+ receta manual de cada guía hace exactamente lo mismo. Para el resto,
44
+ `bvsala doctor` imprime el bloque exacto de cada terminal instalado.
45
+
46
+ ## Lo común a todas las guías
47
+
48
+ ### 1 · Instalar y arrancar el gateway
49
+
50
+ ```bash
51
+ npm i -g @bivelio/savings-layer # trae el binario `bvsala`
52
+ export BIVELIO_SERVICE_KEY=bvk_… # clave de servicio, desde tu panel. Sin ella: reenvía, NO mide
53
+ bvsala gateway # escucha en http://127.0.0.1:8402
54
+ bvsala gateway --puerto 8500 # otro puerto si el 8402 está ocupado
55
+ ```
56
+
57
+ Al arrancar imprime dos líneas; la segunda te dice si estás midiendo:
58
+
59
+ ```
60
+ [bvsala gateway] v0.4.4 escuchando en http://127.0.0.1:8402
61
+ [bvsala gateway] medición: ACTIVA (clave de servicio presente)
62
+ ```
63
+
64
+ `medición: APAGADA (falta BIVELIO_SERVICE_KEY)` = el terminal funciona igual,
65
+ pero nada llega a tu panel.
66
+
67
+ Un gateway habla con **un** upstream (`BVSALA_GATEWAY_UPSTREAM`, por defecto
68
+ `https://api.anthropic.com`). Los terminales del dialecto Anthropic comparten
69
+ instancia; Codex necesita la suya contra OpenAI — `bvsala codex` la arranca
70
+ sola con el upstream correcto según tu modo de auth (ver su guía).
71
+
72
+ ### 2 · Comprobar que mide: el log del gateway
73
+
74
+ Por cada llamada de inferencia servida y medida, el gateway escribe una línea
75
+ con el coste:
76
+
77
+ ```
78
+ [gateway] POST /v1/messages · <modelo> · $0.001234 (in 120, cache↑0 cache↓46792, out 8)
79
+ ```
80
+
81
+ Si ves esa línea con `$`, el terminal está enrutado y medido. Lo que significan
82
+ las otras líneas:
83
+
84
+ | Línea en el log | Qué pasa | Arreglo |
85
+ |---|---|---|
86
+ | `[gateway] POST /v1/v1/messages → 404` | el base URL lleva `/v1` **de más** para ese terminal | quita el `/v1` |
87
+ | `[gateway] POST /messages → 404` | al base URL le **falta** `/v1` (el SDK solo añade `/messages`) | añade el `/v1` |
88
+ | `[bvsala gateway] el modelo "…" no está en el catálogo de precios; su uso se reenvía pero no se mide.` | modelo fuera de catálogo (a propósito: presente-y-mal es peor que ausente) | nada; se reenvía sin medir |
89
+ | 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 |
90
+
91
+ Claude Code manda además un `HEAD /api/hello` antes de la primera llamada: es
92
+ la sonda de vida, no una inferencia.
93
+
94
+ `bvsala doctor [--puerto N]`: sondea el PATH y la existencia de las configs de
95
+ usuario (nunca lee su contenido) e imprime el bloque exacto por terminal con el
96
+ puerto indicado. Solo imprime: no escribe ninguna configuración.
97
+
98
+ ### 3 · Windows
99
+
100
+ `npm i -g @bivelio/savings-layer` y `bvsala claude` son idénticos en los tres
101
+ sistemas. Lo que cambia es cómo se fija la clave en la sesión — el `export` de
102
+ estas guías es POSIX; en PowerShell:
103
+
104
+ ```powershell
105
+ $env:BIVELIO_SERVICE_KEY = "bvk_…"
106
+ bvsala claude
107
+ ```
108
+
109
+ (En `cmd`: `set BIVELIO_SERVICE_KEY=bvk_…`.) El prefijo `VAR=valor comando` de
110
+ las recetas manuales también es POSIX: en Windows, fija la variable primero y
111
+ lanza después.
112
+
113
+ Y una condición dura: el terminal debe estar instalado con su **instalador
114
+ nativo** (un `claude.exe` / `codex.exe` en el PATH — Claude Code, Codex, Goose
115
+ y Qwen Code lo tienen [V]). El shim `.cmd` que deja `npm i -g <terminal>` no
116
+ puede lanzarse de forma segura con argumentos (CVE-2024-27980) y `bvsala` se
117
+ niega con una guía en vez de fallar con un ENOENT mudo.
118
+
119
+ ### 4 · Regla BYOK, sin excepción
120
+
121
+ Todas las recetas del anillo 2 (Goose, Qwen Code, opencode, Crush, Droid, Cline,
122
+ Kilo) son **API key exclusivamente**. Anthropic prohíbe usar el OAuth de Claude
123
+ Pro/Max en harnesses de terceros (opencode lo documenta; Crush retiró su OAuth en
124
+ ene-2026). El gateway reenviaría un Bearer de suscripción sin inmutarse — y eso
125
+ no se promociona ni se documenta como receta.
126
+
127
+ ### 5 · Wording honesto
128
+
129
+ «Anthropic doesn't endorse, maintain, or audit third-party gateway products»
130
+ [V]. El modo BV-SALA es **configuración soportada del binario sin modificar**:
131
+ di eso, nunca «aprobado por Anthropic». El contrato del gateway es una lista
132
+ abierta de cabeceras y campos que crece con cada release del terminal; el
133
+ passthrough byte a byte es lo que lo mantiene vivo sin tocar nada.
@@ -0,0 +1,110 @@
1
+ # Claude Code a través del gateway
2
+
3
+ **Fuente:** `docs/PASARELA-S3-TERMINALES.md` §1 (conformidad con el protocolo
4
+ oficial de gateway de Claude Code, leído íntegro el 2026-08-30). Es el terminal
5
+ de referencia: 10 de 12 requisitos del protocolo los cumple el passthrough por
6
+ construcción, con test.
7
+
8
+ ## Qué necesitas
9
+
10
+ - `claude` en el PATH.
11
+ - **Una API key de Anthropic** (`ANTHROPIC_API_KEY`, o `ANTHROPIC_AUTH_TOKEN` /
12
+ `apiKeyHelper`): es el segmento que BV-SALA mide y factura. Con login de
13
+ claude.ai el enrutado también funciona — la doc oficial lo sanciona: el login
14
+ sigue siendo la credencial activa y «its usage limits and billing apply» [V] —
15
+ pero ahí no hay factura por token que recortar.
16
+ - `bvsala` instalado y `BIVELIO_SERVICE_KEY` exportada (ver [README](README.md)).
17
+
18
+ ## Envoltorio: `bvsala claude` (recomendado)
19
+
20
+ ```bash
21
+ export BIVELIO_SERVICE_KEY=bvk_…
22
+ bvsala claude # cualquier argumento pasa tal cual: bvsala claude -p "hola"
23
+ ```
24
+
25
+ Arranca el gateway en un **puerto efímero**, lanza `claude` con
26
+ `ANTHROPIC_BASE_URL=http://127.0.0.1:<puerto>` en su entorno, hereda tu
27
+ terminal entera (el TUI funciona tal cual) y apaga el gateway cuando `claude`
28
+ sale. No escribe ninguna configuración. No toca credenciales: el reenvío de
29
+ `Authorization` / `x-api-key` es íntegro.
30
+
31
+ ## Manual: `ANTHROPIC_BASE_URL`
32
+
33
+ Si prefieres un gateway compartido en un puerto fijo (por ejemplo para varios
34
+ terminales a la vez):
35
+
36
+ ```bash
37
+ bvsala gateway # ventana 1: http://127.0.0.1:8402
38
+ ANTHROPIC_BASE_URL=http://127.0.0.1:8402 claude # ventana 2 — SIN /v1
39
+ ```
40
+
41
+ Claude Code añade `/v1/messages` por su cuenta. Sin barra final.
42
+
43
+ ## Comprobar que funciona
44
+
45
+ En el log del gateway, en este orden:
46
+
47
+ ```
48
+ [gateway] HEAD /api/hello → 200
49
+ [gateway] POST /v1/messages · <modelo> · $0.001234 (in 120, cache↑0 (1h 49268) cache↓43430, out 8)
50
+ ```
51
+
52
+ El `HEAD /api/hello` es la sonda de vida que Claude Code manda antes de la
53
+ primera llamada. La línea con `$` es la inferencia medida. La cifra `cache↑ …
54
+ (1h …)` es normal: Claude Code escribe caché a 1 h, y es más de la mitad de la
55
+ entrada en sesiones reales.
56
+
57
+ ## Model discovery (opcional, solo con API key)
58
+
59
+ ```bash
60
+ CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 bvsala claude
61
+ ```
62
+
63
+ Con la bandera, Claude Code pide `GET /v1/models?limit=1000` al gateway
64
+ (timeout 3 s, credencial en `Authorization` **y** `x-api-key`) y filtra ids que
65
+ contengan «claude»/«anthropic» [V]. El passthrough lo sirve por construcción
66
+ (api.anthropic.com responde `{data:[{id,display_name}]}`) y en `/model` debería
67
+ aparecer «From gateway».
68
+
69
+ Honestidad sobre el estado: el passthrough de `GET /v1/models` **tiene test**
70
+ (pasa íntegro y no factura); la validación en mano con API key está pendiente
71
+ (S3 §1.2). Y las únicas fuentes de credencial
72
+ del discovery son `ANTHROPIC_AUTH_TOKEN`/`apiKeyHelper` y `ANTHROPIC_API_KEY`: un
73
+ login claude.ai **no** es fuente, así que en suscripción pura el cliente escribe
74
+ `[gatewayDiscovery] skipped` y ni pregunta.
75
+
76
+ ## La trampa típica
77
+
78
+ 1. **`/v1` en el base URL.** `ANTHROPIC_BASE_URL=http://127.0.0.1:8402/v1` da
79
+ `[gateway] POST /v1/v1/messages → 404` y Claude Code reporta un error
80
+ genérico. Sin `/v1`.
81
+ 2. **Watchdog de 300 s.** En conexiones por `ANTHROPIC_BASE_URL` Claude Code
82
+ corta un stream sin bytes durante 300 s (único valor que documenta hoy la doc
83
+ oficial). En el camino normal el gateway no bufferiza y reenvía los pings
84
+ como bytes, así que no salta. Solo bajo `BVSALA_GATEWAY_COALESCE=1` (apagada
85
+ por defecto) una petición gemela espera a que termine la primaria, y una
86
+ primaria de más de 300 s la mataría — está identificado en S3 §1.1 y no se
87
+ enciende por defecto hasta arreglarlo.
88
+ 3. **Redirecciones.** Cualquier 3xx en el base URL es fallo de discovery por
89
+ diseño del cliente; el gateway reenvía el 3xx en vez de seguirlo con tu
90
+ credencial (`redirect: "manual"`, verificado).
91
+
92
+ ## Lo que NO pasa por el gateway (aviso honesto)
93
+
94
+ La promesa de medición es «todo el tráfico de **inferencia**», no «todo el
95
+ tráfico»: **fast-mode y el safety-check de WebFetch van directos a
96
+ api.anthropic.com por diseño del cliente** [V]. Ese gasto no aparece en tu panel
97
+ y no es computable desde el gateway. El contrato es una lista abierta que crece
98
+ con cada release de Claude Code; BV-SALA repite el smoke test de `bvsala claude`
99
+ contra cada versión nueva.
100
+
101
+ Las cabeceras `x-claude-code-session-id` / `agent-id` / `parent-agent-id`
102
+ llegan en el tráfico real y hoy se reenvían sin consumirlas: el coste por sesión
103
+ y por subagente está previsto en S3 §1.3, todavía no en el panel.
104
+
105
+ ## Nota BYOK y wording
106
+
107
+ El modo BV-SALA es **configuración soportada del binario sin modificar**, nunca
108
+ «aprobado por Anthropic» («Anthropic doesn't endorse, maintain, or audit
109
+ third-party gateway products» [V]). Lo que BV-SALA mide y factura es el uso por
110
+ API key; el gateway no persiste ninguna credencial, sea cual sea.
@@ -0,0 +1,53 @@
1
+ # Cline a través del gateway
2
+
3
+ **Fuente:** `docs/PASARELA-S3-TERMINALES.md` §3 [V]. Cline configura el
4
+ proveedor por **UI** (extensión de VS Code), así que aquí no hay archivo ni
5
+ envoltorio: solo pasos y la forma exacta del URL, que es **con `/v1`**.
6
+
7
+ Por qué con `/v1` (refutación verificada): Cline `main` ya usa
8
+ `@ai-sdk/anthropic`, no el SDK TS oficial; su `normalizeBaseURL` solo añade
9
+ `/v1` cuando el URL es exactamente `https://api.anthropic.com`, y `builtins.ts`
10
+ fija `https://api.anthropic.com/v1` [V]. Con un URL custom, lo que pongas es lo
11
+ que se usa.
12
+
13
+ ## Qué necesitas
14
+
15
+ - Cline instalado en VS Code.
16
+ - Tu **API key** de Anthropic.
17
+ - `bvsala gateway` corriendo en `http://127.0.0.1:8402` (ver [README](README.md)).
18
+
19
+ ## Pasos en la UI
20
+
21
+ 1. Abre los ajustes de Cline → **API Provider**: elige **Anthropic**.
22
+ 2. Pega tu **Anthropic API Key**.
23
+ 3. Marca **«Use custom base URL»** y escribe exactamente:
24
+
25
+ ```
26
+ http://127.0.0.1:8402/v1
27
+ ```
28
+
29
+ **Con `/v1`, sin barra final.**
30
+ 4. Elige el modelo y guarda.
31
+
32
+ ## Comprobar que funciona
33
+
34
+ Manda un mensaje trivial desde Cline y mira la ventana del gateway:
35
+
36
+ ```
37
+ [gateway] POST /v1/messages · <modelo> · $0.001234 (in 120, cache↑0 cache↓0, out 8)
38
+ ```
39
+
40
+ ## La trampa típica
41
+
42
+ - **Sin `/v1`** → Cline pide `…/messages` → `[gateway] POST /messages → 404` y
43
+ la UI muestra un error genérico de API. Añade el `/v1`.
44
+ - **Barra final** → no.
45
+ - **Gateway apagado**: la UI recuerda el base URL custom; cuando no tengas
46
+ `bvsala gateway` corriendo, Cline fallará hasta que lo arranques o desmarques
47
+ la casilla.
48
+
49
+ ## Nota BYOK
50
+
51
+ **API key exclusivamente.** Anthropic prohíbe usar el OAuth de Claude Pro/Max en
52
+ harnesses de terceros (Cline estuvo en la primera oleada del bloqueo de
53
+ ene-2026). Esta receta es la del proveedor Anthropic con API key; nada más.
@@ -0,0 +1,132 @@
1
+ # Codex CLI a través del gateway
2
+
3
+ **Fuente:** `docs/PASARELA-S3-TERMINALES.md` §2 (código de `openai/codex`,
4
+ `openai-node` y docs de OpenAI, 2026-08-30). **Estado: envoltorio en el
5
+ paquete y validado en mano el 30-08** (sesión real de Codex v0.134 con auth
6
+ ChatGPT: 426 → fallback HTTP, stream íntegro, registro `measured` en la
7
+ ingesta). Lee «Qué mide» y las validaciones pendientes antes de anunciar
8
+ cifras.
9
+
10
+ ## Qué necesitas
11
+
12
+ - `codex` en el PATH.
13
+ - Login en Codex: **API key de OpenAI** (el caso que se monetiza) o
14
+ suscripción ChatGPT (se enruta y se mide, pero **no se monetiza**: no hay
15
+ factura por tokens; ver más abajo).
16
+ - `bvsala` instalado y `BIVELIO_SERVICE_KEY` exportada (ver [README](README.md)).
17
+
18
+ ## Envoltorio: `bvsala codex [args…]`
19
+
20
+ ```bash
21
+ export BIVELIO_SERVICE_KEY=bvk_…
22
+ bvsala codex # cualquier argumento pasa tal cual a codex
23
+ ```
24
+
25
+ Espejo de `bvsala claude`: gateway efímero con el upstream que toque +
26
+ `spawn("codex", ["-c", 'openai_base_url="http://127.0.0.1:<puerto><prefijo>"', ...resto])`
27
+ (cadena TOML entrecomillada: «values are parsed as TOML», doc oficial). Por qué
28
+ así y no por variable de entorno:
29
+
30
+ - `-c` es flag global de **máxima precedencia** [V]; **no existe** una env
31
+ `OPENAI_BASE_URL` en Codex (cero referencias en su código de config) [V].
32
+ - Sin escribir config, el ajuste muere con el proceso: nada queda apuntando a un
33
+ gateway que ya no corre.
34
+ - **El prefijo depende del modo de auth** [V `AuthDotJson`, `to_api_provider`]:
35
+ `ApiKey` → upstream `https://api.openai.com`, prefijo `/v1`; toda la familia
36
+ ChatGPT → upstream `https://chatgpt.com`, prefijo `/backend-api/codex`. El
37
+ envoltorio lee `auth_mode` de `~/.codex/auth.json` **por existencia/valor del
38
+ enum, jamás imprime tokens**; `BVSALA_CODEX_AUTH=api-key|chatgpt` lo fuerza.
39
+
40
+ ## Manual (API key): `-c openai_base_url=…`
41
+
42
+ ```bash
43
+ BVSALA_GATEWAY_UPSTREAM=https://api.openai.com bvsala gateway # ventana 1: http://127.0.0.1:8402
44
+ codex -c openai_base_url=http://127.0.0.1:8402/v1 # ventana 2 — CON /v1, sin barra final
45
+ ```
46
+
47
+ Codex añade `/responses` (`POST {base_url}/responses`, siempre `stream: true`)
48
+ [V]. Si en el 8402 ya tienes el gateway de Anthropic para otros terminales,
49
+ arranca este en otro puerto (`--puerto 8403`) y apunta ahí: un gateway habla con
50
+ un solo upstream.
51
+
52
+ No lo escribas en `~/.codex/config.toml`: ahí se quedaría fijo y Codex fallaría
53
+ cada vez que el gateway no esté corriendo. El `-c` por sesión es la forma.
54
+
55
+ ## Comprobar que funciona
56
+
57
+ En el log del gateway verás, en este orden, la sesión real (30-08):
58
+
59
+ ```
60
+ [gateway] GET /backend-api/codex/models → 200
61
+ [gateway] GET /backend-api/codex/responses → 426 (WebSocket no soportado; el cliente cae a HTTP)
62
+ [gateway] POST /backend-api/codex/responses → el cliente cerró tras el usage; se mide igual
63
+ [gateway] POST /backend-api/codex/responses · gpt-5.5 · $0.210766 (in 41880, cache↑0 cache↓2432, out 5)
64
+ ```
65
+
66
+ (Con API key la ruta es `/v1/responses`.) La línea con `$` es la medición.
67
+
68
+ **Qué mide:** el `usage` de `event: response.completed` con la fórmula oficial
69
+ de OpenAI (`entrada = input − cached − writes`; `cached` a la tarifa cacheada
70
+ del catálogo; `writes` a 1,25×, la prima oficial de gpt-5.6+) [V]. Tres cosas
71
+ que el tráfico real enseñó y el gateway ya contempla:
72
+
73
+ - Codex **cierra la conexión nada más recibir `response.completed`**; el
74
+ gateway mide igual con el usage ya observado («se mide igual» en el log).
75
+ - El backend de ChatGPT sirve el SSE **sin cabecera `content-type`**; en las
76
+ rutas `…/responses` el gateway asume SSE si falta.
77
+ - Codex manda el cuerpo de la petición **comprimido con zstd**; el gateway lo
78
+ reenvía intacto y lee el `model` a través del `content-encoding` (Node ≥ 22.15
79
+ para zstd; en un Node sin zstd la petición pasa y no se mide).
80
+
81
+ ## La trampa típica
82
+
83
+ 1. **El prefijo.** Con API key es `/v1` **exacto**. Con la familia ChatGPT el
84
+ base_url tiene que terminar **exactamente** en `/backend-api/codex` o
85
+ `supports_codex_backend_routes()` se apaga [V]. Mezclarlos = 404 silencioso.
86
+ 2. **Una línea `failed to connect to websocket: HTTP error: 426`** al arrancar
87
+ es normal: Codex intenta WebSocket primero (`supports_websockets: true` está
88
+ hardcodeado y no se apaga por config [V]); el gateway responde `426 Upgrade
89
+ Required` y el fallback a HTTP es inmediato y pegajoso por sesión [V test
90
+ `websocket_fallback.rs`]. Sin «Reconnecting…».
91
+ 3. **`failed to refresh available models: unknown variant …`** es de tu versión
92
+ de Codex frente a su backend: aparece igual **sin** gateway (comprobado).
93
+ 4. **Barra final.** `…/v1/` no.
94
+
95
+ ## Modo suscripción ChatGPT — validado, medido, NO monetizado
96
+
97
+ Validado en mano el 30-08 (prefijo `/backend-api/codex`, upstream
98
+ `https://chatgpt.com`): 200, stream íntegro, `usage` presente en
99
+ `response.completed` y registro `measured` en la ingesta. **Pero un suscriptor
100
+ no paga por tokens**: la cifra que ve es el equivalente a precio de lista por
101
+ 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 se monetiza** — igual que el OAuth de
103
+ suscripción en Claude Code (`docs/PASARELA-F0.md`: «el caso que NO
104
+ facturamos»). OpenAI ni bendice ni prohíbe el proxy de suscripción (silencio
105
+ del staff, a diferencia de Anthropic, que lo documenta). Postura idéntica a
106
+ Claude Code: cero persistencia de `Authorization` / `ChatGPT-Account-ID`,
107
+ binario sin modificar, y no se promociona como modo de ahorro.
108
+
109
+ **Laguna conocida:** el registro que emite el gateway para un suscriptor es hoy
110
+ **idéntico** al de una API key (`basis: measured`, coste a precio de lista) —
111
+ el panel no puede distinguirlos. Mientras la comisión sea 0 no hay efecto; antes
112
+ de que S4 calcule comisión sobre ahorro medido, S2 etiquetará la auth de
113
+ suscripción en el wire para que ese tráfico jamás entre en la base. Está en el
114
+ plan (`docs/PLAN-PASARELA-API-KEY.md`, S3).
115
+
116
+ ## Validaciones en mano
117
+
118
+ - ✅ (b) Suscripción ChatGPT: 426 → fallback, 200, stream íntegro, `usage`
119
+ presente, registro `measured` (30-08).
120
+ - ⏳ (a) Sesión con **API key** (ruta `/v1/responses` contra api.openai.com):
121
+ misma mecánica, pendiente de una cuenta con API key.
122
+ - ⏳ (c) Plausibilidad `cached + writes ≤ input` en un prompt repetido > 1.024
123
+ tokens (OpenAI reconoció contadores mal en el arranque de 5.6). El gateway
124
+ ya recorta al lado que infra-mide si no cuadra.
125
+ - ⏳ Contrastar la cifra del gateway con la consola del proveedor en una sesión
126
+ larga (Codex imprime su propio «tokens used»; la fuente de BV-SALA es el
127
+ `usage` del proveedor, no ese contador).
128
+
129
+ ## Nota BYOK
130
+
131
+ La receta documentada es **API key exclusivamente**. El gateway no persiste
132
+ ninguna credencial; lo que factura BV-SALA es el uso por API key.
@@ -0,0 +1,52 @@
1
+ # Crush a través del gateway
2
+
3
+ **Fuente:** `docs/PASARELA-S3-TERMINALES.md` §3 [V]. Crush (Charm) configura
4
+ proveedores por **`~/.config/crush/crushrc`** (un script Bash); `crush.json`
5
+ está **deprecado** (verificado 2026-08-30). Su SDK Go resuelve rutas de forma
6
+ relativa, y eso dicta la forma del URL: **con `/v1`, sin barra final**.
7
+
8
+ ## Qué necesitas
9
+
10
+ - `crush` instalado.
11
+ - `ANTHROPIC_API_KEY` exportada (Crush retiró su OAuth de Claude en ene-2026:
12
+ solo API key).
13
+ - `bvsala` instalado y `BIVELIO_SERVICE_KEY` exportada (ver [README](README.md)).
14
+
15
+ ## Config de usuario: `~/.config/crush/crushrc`
16
+
17
+ Añade esta línea:
18
+
19
+ ```bash
20
+ provider add bvsala --type anthropic --base-url http://127.0.0.1:8402/v1 --api-key "$ANTHROPIC_API_KEY"
21
+ ```
22
+
23
+ Y elige el proveedor `bvsala` en Crush. `crush.json` no: deprecado.
24
+
25
+ No hay envoltorio `bvsala crush`: el enrutado es config de usuario. El
26
+ `bvsala doctor --write crush` previsto en S3 hará un append comentado de esa
27
+ línea en tu `crushrc` (con copia `.bak`).
28
+
29
+ ## Arrancar y comprobar
30
+
31
+ ```bash
32
+ bvsala gateway # http://127.0.0.1:8402
33
+ crush
34
+ ```
35
+
36
+ ```
37
+ [gateway] POST /v1/messages · <modelo> · $0.001234 (in 120, cache↑0 cache↓0, out 8)
38
+ ```
39
+
40
+ ## La trampa típica
41
+
42
+ - **Barra final.** `--base-url http://127.0.0.1:8402/v1/` → el SDK Go resuelve
43
+ relativo y pide `/v1/v1/messages` → `[gateway] POST /v1/v1/messages → 404` [V].
44
+ Sin barra.
45
+ - **Sin `/v1`** → `[gateway] POST /messages → 404`.
46
+ - **`crush.json`** → deprecado; si tienes uno viejo con otro proveedor, puede
47
+ seguir mandando. Usa `crushrc`.
48
+
49
+ ## Nota BYOK
50
+
51
+ **API key exclusivamente.** Crush ya retiró su OAuth de Claude; el gateway
52
+ reenviaría cualquier credencial sin inmutarse, pero la receta es esta.
@@ -0,0 +1,55 @@
1
+ # Droid (Factory) a través del gateway
2
+
3
+ **Fuente:** `docs/PASARELA-S3-TERMINALES.md` §3. Droid es **cerrado**; el
4
+ mecanismo de enrutado está verificado [V] (`~/.factory/settings.json` →
5
+ `customModels[]` con `provider: "anthropic"`), pero **la forma del base URL es
6
+ inferida [I]: hay que validarla en mano contra el log del gateway.** Esta guía
7
+ lo dice tal cual; no hay validación hecha.
8
+
9
+ ## Qué necesitas
10
+
11
+ - `droid` instalado, en modo BYOK con tu **API key** de Anthropic.
12
+ - `bvsala` instalado y `BIVELIO_SERVICE_KEY` exportada (ver [README](README.md)).
13
+
14
+ ## Config de usuario: `~/.factory/settings.json`
15
+
16
+ ```json
17
+ {
18
+ "customModels": [
19
+ {
20
+ "provider": "anthropic",
21
+ "baseUrl": "http://127.0.0.1:8402",
22
+ "apiKey": "<tu ANTHROPIC_API_KEY>"
23
+ }
24
+ ]
25
+ }
26
+ ```
27
+
28
+ Las claves verificadas [V] son `customModels[]{provider:"anthropic", baseUrl,
29
+ apiKey}`. La forma **sin `/v1`** es inferida [I]. Conserva el resto de tu
30
+ `settings.json` (el `doctor --write droid` previsto en S3 hará el merge JSON
31
+ conservador con copia `.bak`).
32
+
33
+ ## Validar en mano (obligatorio antes de fiarte)
34
+
35
+ 1. `bvsala gateway` en una ventana.
36
+ 2. Lanza `droid` con ese modelo custom y haz una pregunta trivial.
37
+ 3. Mira el log del gateway:
38
+ - `[gateway] POST /v1/messages · <modelo> · $…` → la forma sin `/v1` es la
39
+ buena. Confírmalo en S3 (pasar el [I] a [V]).
40
+ - `[gateway] POST /messages → 404` → Droid solo añade `/messages`: cambia el
41
+ `baseUrl` a `http://127.0.0.1:8402/v1` y repite.
42
+ - nada en el log → Droid no está leyendo el modelo custom; revisa que sea el
43
+ modelo seleccionado.
44
+
45
+ ## La trampa típica
46
+
47
+ - **Adivinar el `/v1`**: por eso el paso de validación. Un URL mal formado
48
+ falla en silencio (404 del upstream sin medir).
49
+ - **La API key va en el JSON**: es config de usuario en tu máquina; no la
50
+ copies a ningún repo ni la pegues en un ticket.
51
+
52
+ ## Nota BYOK
53
+
54
+ **API key exclusivamente.** Droid es BYOK; Anthropic prohíbe el OAuth de Claude
55
+ Pro/Max en harnesses de terceros, y esta receta no lo contempla.
@@ -0,0 +1,54 @@
1
+ # Goose a través del gateway
2
+
3
+ **Fuente:** `docs/PASARELA-S3-TERMINALES.md` §3 [V]. Goose habla dialecto
4
+ Anthropic puro y autentica con `x-api-key` + `anthropic-version`, que el
5
+ reenvío pasa intactos: **nada que cambiar en el gateway**, todo está en la
6
+ forma del URL.
7
+
8
+ ## Qué necesitas
9
+
10
+ - `goose` en el PATH, con el proveedor Anthropic elegido.
11
+ - `ANTHROPIC_API_KEY` exportada (Goose es solo BYOK).
12
+ - `bvsala` instalado y `BIVELIO_SERVICE_KEY` exportada (ver [README](README.md)).
13
+
14
+ ## Envoltorio: `bvsala goose`
15
+
16
+ ```bash
17
+ export BIVELIO_SERVICE_KEY=bvk_…
18
+ bvsala goose # espejo exacto de `bvsala claude`
19
+ ```
20
+
21
+ Espejo exacto de `bvsala claude`: gateway efímero, `goose` lanzado con
22
+ `ANTHROPIC_HOST=http://127.0.0.1:<puerto>`, todo muere al salir. La tabla vive
23
+ en `gateway/envoltorios.ts`. La receta manual de abajo es literalmente lo mismo.
24
+
25
+ ## Manual: `ANTHROPIC_HOST`
26
+
27
+ ```bash
28
+ bvsala gateway # ventana 1: http://127.0.0.1:8402
29
+ ANTHROPIC_HOST=http://127.0.0.1:8402 goose # ventana 2 — SIN /v1
30
+ ```
31
+
32
+ `ANTHROPIC_HOST` **pisa** lo que haya en `config.yaml` [V], así que no hace
33
+ falta tocar ningún archivo. Sin `/v1`, sin barra final: Goose añade
34
+ `/v1/messages` por su cuenta.
35
+
36
+ ## Comprobar que funciona
37
+
38
+ ```
39
+ [gateway] POST /v1/messages · <modelo> · $0.001234 (in 120, cache↑0 cache↓0, out 8)
40
+ ```
41
+
42
+ ## La trampa típica
43
+
44
+ - **`/v1` en `ANTHROPIC_HOST`** → `[gateway] POST /v1/v1/messages → 404`. Quítalo.
45
+ - **Escribir el host en `config.yaml`** en vez de la variable: funciona, pero
46
+ se queda fijo y Goose falla cada vez que el gateway no esté corriendo. La
47
+ variable de entorno por sesión es la forma; la env pisa al archivo de todos
48
+ modos.
49
+
50
+ ## Nota BYOK
51
+
52
+ **API key exclusivamente.** Anthropic prohíbe usar el OAuth de Claude Pro/Max
53
+ en harnesses de terceros; Goose es BYOK y esta receta también. El gateway
54
+ reenviaría cualquier credencial sin inmutarse — eso no es una receta.
@@ -0,0 +1,50 @@
1
+ # Kilo Code a través del gateway
2
+
3
+ **Fuente:** `docs/PASARELA-S3-TERMINALES.md` §3. Kilo Code configura por **UI**
4
+ (proveedor custom con Provider API «Anthropic Messages»; la config queda en
5
+ `kilo.json`, detectable) [V]. **La forma del base URL es inferida [I] por
6
+ linaje Cline: probar con `/v1` primero y validar en mano contra el log del
7
+ gateway.** No hay validación hecha; esta guía lo dice tal cual.
8
+
9
+ ## Qué necesitas
10
+
11
+ - Kilo Code instalado en VS Code.
12
+ - Tu **API key** de Anthropic.
13
+ - `bvsala gateway` corriendo en `http://127.0.0.1:8402` (ver [README](README.md)).
14
+
15
+ ## Pasos en la UI
16
+
17
+ 1. Ajustes de Kilo Code → añade un **proveedor custom**.
18
+ 2. **Provider API**: «Anthropic Messages».
19
+ 3. Pega tu **API key** de Anthropic.
20
+ 4. **Base URL** — prueba primero:
21
+
22
+ ```
23
+ http://127.0.0.1:8402/v1
24
+ ```
25
+
26
+ **Sin barra final.**
27
+ 5. Elige el modelo y guarda.
28
+
29
+ ## Validar en mano (obligatorio antes de fiarte)
30
+
31
+ Manda un mensaje trivial y mira la ventana del gateway:
32
+
33
+ - `[gateway] POST /v1/messages · <modelo> · $…` → **con `/v1`** es la forma
34
+ buena; confírmalo en S3 (pasar el [I] a [V]).
35
+ - `[gateway] POST /v1/v1/messages → 404` → Kilo ya añade `/v1`: quita el `/v1`
36
+ del base URL (`http://127.0.0.1:8402`) y repite.
37
+ - `[gateway] POST /messages → 404` → falta el `/v1`; vuelve a ponerlo.
38
+ - nada en el log → el proveedor custom no es el activo.
39
+
40
+ ## La trampa típica
41
+
42
+ - **Adivinar el `/v1`**: de ahí el paso de validación. Un URL mal formado falla
43
+ en silencio (404 del upstream, nada medido, error genérico en la UI).
44
+ - **Gateway apagado**: `kilo.json` recuerda el base URL; sin `bvsala gateway`
45
+ corriendo, Kilo fallará hasta que lo arranques o cambies de proveedor.
46
+
47
+ ## Nota BYOK
48
+
49
+ **API key exclusivamente.** Anthropic prohíbe usar el OAuth de Claude Pro/Max en
50
+ harnesses de terceros; esta receta es la del proveedor custom con API key.
@@ -0,0 +1,67 @@
1
+ # opencode a través del gateway
2
+
3
+ **Fuente:** `docs/PASARELA-S3-TERMINALES.md` §3 [V]. opencode enruta por
4
+ **archivo de configuración**, no por variable de entorno, y su capa
5
+ `packages/llm` añade `/messages` al `baseURL`: por eso aquí el URL lleva `/v1`.
6
+
7
+ ## Qué necesitas
8
+
9
+ - `opencode` instalado, proveedor `anthropic` con tu **API key** de Anthropic.
10
+ - `bvsala` instalado y `BIVELIO_SERVICE_KEY` exportada (ver [README](README.md)).
11
+
12
+ ## Config de usuario: `~/.config/opencode/opencode.json`
13
+
14
+ ```json
15
+ {
16
+ "provider": {
17
+ "anthropic": {
18
+ "options": {
19
+ "baseURL": "http://127.0.0.1:8402/v1"
20
+ }
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ **Con `/v1`, sin barra final.** Es la clave `provider.anthropic.options.baseURL`
27
+ [V]. El `opencode.json` de proyecto pisa al global: si tienes uno en el repo,
28
+ ahí es donde manda.
29
+
30
+ No hay envoltorio `bvsala opencode`: el enrutado es config de usuario. El
31
+ `bvsala doctor --write opencode` previsto en S3 hará un merge JSON conservador
32
+ de este bloque (con copia `.bak`) en tu config de usuario, nunca en archivos
33
+ del repo.
34
+
35
+ ### Alternativa [V]: `baseURL` desde variable de entorno
36
+
37
+ ```json
38
+ { "provider": { "anthropic": { "options": { "baseURL": "{env:BVSALA_BASE_URL}" } } } }
39
+ ```
40
+
41
+ Escrito una vez, habilita un uso puro-env (`BVSALA_BASE_URL=http://127.0.0.1:8402/v1 opencode`).
42
+ **Trampa:** sin la variable, el `baseURL` queda vacío y opencode no funciona.
43
+
44
+ ## Arrancar y comprobar
45
+
46
+ ```bash
47
+ bvsala gateway # http://127.0.0.1:8402
48
+ opencode
49
+ ```
50
+
51
+ ```
52
+ [gateway] POST /v1/messages · <modelo> · $0.001234 (in 120, cache↑0 cache↓0, out 8)
53
+ ```
54
+
55
+ ## La trampa típica
56
+
57
+ - **Sin `/v1`** → opencode pide `http://127.0.0.1:8402/messages` → `[gateway] POST /messages → 404`.
58
+ - **Barra final** (`…/v1/`) → no.
59
+ - **Config de proyecto** con otro `baseURL` pisando al global sin que lo sepas.
60
+ - **Gateway apagado con la config escrita**: opencode falla hasta que arranques
61
+ `bvsala gateway` de nuevo. La config es persistente; el gateway, no. (La
62
+ variante `{env:…}` sin exportar la variable también falla, por vacío.)
63
+
64
+ ## Nota BYOK
65
+
66
+ **API key exclusivamente.** opencode documenta expresamente que el OAuth de
67
+ Claude Pro/Max no puede usarse en harnesses de terceros; esta receta lo respeta.
@@ -0,0 +1,76 @@
1
+ # Qwen Code a través del gateway
2
+
3
+ **Fuente:** `docs/PASARELA-S3-TERMINALES.md` §3 [V]. Qwen Code con proveedor
4
+ Anthropic habla dialecto Anthropic puro (SDK TS oficial), así que el medidor
5
+ actual lo entiende sin cambios. La familia de modelos **qwen** queda fuera del
6
+ catálogo a propósito (más abajo).
7
+
8
+ ## Qué necesitas
9
+
10
+ - `qwen` en el PATH.
11
+ - **Una API key de Anthropic** (`ANTHROPIC_API_KEY`), con Qwen Code configurado
12
+ para usar el proveedor Anthropic.
13
+ - `bvsala` instalado y `BIVELIO_SERVICE_KEY` exportada (ver [README](README.md)).
14
+
15
+ ## Config de usuario: `~/.qwen/settings.json`
16
+
17
+ El enrutado necesita dos cosas a la vez [V]: la variable de entorno
18
+ `ANTHROPIC_BASE_URL` **y** que `~/.qwen/settings.json` seleccione el tipo
19
+ Anthropic:
20
+
21
+ ```json
22
+ {
23
+ "selectedType": "anthropic"
24
+ }
25
+ ```
26
+
27
+ Conserva el resto de tu `settings.json`; la clave verificada en S3 es
28
+ `selectedType`. Sin ella, Qwen Code no lee `ANTHROPIC_BASE_URL` y el gateway no
29
+ verá nada.
30
+
31
+ ## Envoltorio: `bvsala qwen`
32
+
33
+ ```bash
34
+ export BIVELIO_SERVICE_KEY=bvk_…
35
+ bvsala qwen
36
+ ```
37
+
38
+ Espejo de `bvsala claude` con `ANTHROPIC_BASE_URL`. El envoltorio **no lee**
39
+ `settings.json`: comprueba tú que `selectedType` sea `anthropic` (`bvsala
40
+ doctor` te recuerda la ruta). La receta manual de abajo es lo mismo.
41
+
42
+ ## Manual: `ANTHROPIC_BASE_URL`
43
+
44
+ ```bash
45
+ bvsala gateway # ventana 1: http://127.0.0.1:8402
46
+ ANTHROPIC_BASE_URL=http://127.0.0.1:8402 qwen # ventana 2 — SIN /v1
47
+ ```
48
+
49
+ Sin `/v1`: el SDK TS oficial de Anthropic añade `/v1/messages` [V]. Sin barra
50
+ final.
51
+
52
+ ## Comprobar que funciona
53
+
54
+ ```
55
+ [gateway] POST /v1/messages · <modelo> · $0.001234 (in 120, cache↑0 cache↓0, out 8)
56
+ ```
57
+
58
+ ## La trampa típica
59
+
60
+ 1. **`/v1` en el base URL** → `[gateway] POST /v1/v1/messages → 404`.
61
+ 2. **`selectedType` no es `anthropic`** → el log del gateway no muestra nada:
62
+ Qwen Code está hablando con su proveedor por defecto, no contigo.
63
+ 3. **Modelos qwen (DashScope / ModelStudio)**: están **fuera del catálogo a
64
+ propósito** — `costeMedido → null`, passthrough con un aviso una vez
65
+ (`el modelo "…" no está en el catálogo de precios; su uso se reenvía pero no
66
+ se mide`). Coherente con la política del catálogo: presente-y-mal es peor que
67
+ ausente. Para medirlos harían falta precios **verificados** de
68
+ ModelStudio/DashScope en la fecha, y antes comprobar que DashScope emite SSE
69
+ con `\n\n` — el observador parte bloques solo por `\n\n`; un upstream que
70
+ emita `\r\n\r\n` jamás cerraría un bloque y no mediría nada aunque el precio
71
+ estuviera [I].
72
+
73
+ ## Nota BYOK
74
+
75
+ **API key exclusivamente.** Anthropic prohíbe el OAuth de Claude Pro/Max en
76
+ harnesses de terceros; Qwen Code es BYOK desde abr-2026 y esta receta también.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivelio/savings-layer",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "BV-SALA — provider-neutral LLM execution optimizer. A decision layer in front of any LLM provider that cuts the cost of every request while preserving required quality, and reports the saving per call with token-level accounting you can check against your bill. Proprietary and confidential — property of BiVelio Inc.",
5
5
  "keywords": [
6
6
  "llm",
@@ -41,6 +41,7 @@
41
41
  "types": "./dist/index.d.ts",
42
42
  "files": [
43
43
  "dist",
44
+ "docs/terminales",
44
45
  "README.md",
45
46
  "LICENSE"
46
47
  ],