@bivelio/savings-layer 0.5.0-rc.1 → 0.5.0

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
@@ -170,6 +170,9 @@ and the free **Measure** license, which runs the layer in shadow mode on your ow
170
170
  traffic and shows what it would have saved, valued with your provider's counter. The verifier that replays the optimizer on your own data
171
171
  lives in your dashboard once you sign in.
172
172
 
173
+ **Upgrading from 0.4?** Every breaking change of 0.5, with a before and after for
174
+ each, is in [Migrating from 0.4 to 0.5](https://savings.bivelio.com/en/migrate-to-0.5).
175
+
173
176
  ## Set it up in one command
174
177
 
175
178
  ```bash
@@ -914,7 +917,7 @@ Ask for a response contract and you also get `data`, validated against your sche
914
917
  ```ts
915
918
  const { data, text } = await layer.generate<{ answer: number }>({
916
919
  model: "openai/gpt-4o-mini",
917
- messages: [{ role: "user", content: "What is 2 + 2 * 3?" }],
920
+ messages: [{ role: "user", content: "How many legs does a spider have?" }],
918
921
  response: {
919
922
  contractId: "answer:v1",
920
923
  schema: {
@@ -930,6 +933,11 @@ const { data, text } = await layer.generate<{ answer: number }>({
930
933
  console.log(data.answer); // 8
931
934
  ```
932
935
 
936
+ A question the layer answers on its own (the arithmetic of the first example)
937
+ never reaches the model, so its `data` is the resolver's `{ value: 8 }`, not
938
+ your schema's shape: `report.qualityLedger` says the contract was not met.
939
+ Ask the model for anything you want in your contract's shape.
940
+
933
941
  </details>
934
942
 
935
943
  <details>
@@ -79,7 +79,7 @@ td.test(n.betas))return null;let r=ct(e,t);if(r===null||nd(r))return null;let o=
79
79
  let r=Object.entries(ks).map(([o,i])=>`${o}\u2192${i}`).join(", ");return e!=="1"?{linea:"sibling model routing: OFF (BVSALA_GATEWAY_SIBLING_MODEL=1 turns it on)",aviso:false}:n?{linea:`sibling model r\
80
80
  outing: ARMED (${r}; ON if your licence includes sibling_model_routing; fail-closed). Changes the model you see in the response; cache reads cost 0.025\xD7 instead of 0.1\xD7 (list price). Turning it on mid\
81
81
  -conversation pays one cold prefix write, once. If the provider rejects the rewritten request (400), it is re-sent unchanged once`,aviso:false}:{linea:"sibling model routing: NOT ARMED - BVSALA_GATEWAY_E\
82
- LISION=0 turns off the levers",aviso:true}}var xs=new Set;function ee(e,t){xs.has(e)||(xs.add(e),console.warn(t));}var ye="0.5.0-rc.1",fo="x-bvsala-version";var Ut=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=
82
+ LISION=0 turns off the levers",aviso:true}}var xs=new Set;function ee(e,t){xs.has(e)||(xs.add(e),console.warn(t));}var ye="0.5.0",fo="x-bvsala-version";var Ut=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=
83
83
  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){let n=this.now();this.buffer.push({report:t,at:n});
84
84
  let r=rd(t);r&&this.buffer.push({report:r,at:n}),this.buffer.length>this.maxBuffer&&this.buffer.splice(0,this.buffer.length-this.maxBuffer),this.buffer.length>=this.maxBatch&&this.flush();}start(){this.
85
85
  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(t){this.timer&&
package/dist/index.d.ts CHANGED
@@ -154,7 +154,7 @@ declare function precioDeLista(model: string, at?: Date | number): CostModel | u
154
154
  * on the customer's machine. `test/version-declarada.test.ts` ties this
155
155
  * constant to package.json so the two can never drift.
156
156
  */
157
- declare const BV_SALA_VERSION = "0.5.0-rc.1";
157
+ declare const BV_SALA_VERSION = "0.5.0";
158
158
 
159
159
  /** How the layer served a request that is eligible for a verification check. */
160
160
  type VerificationStratum =
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ for(let n of Yp[t]??[])if(!(n.vigenteDesde!==void 0&&e<n.vigenteDesde)&&!(n.vige
28
28
  p-research","o4-mini-deep-research","gpt-5.5-pro","gpt-5.4-pro","gpt-5.2-pro","gpt-5-pro","gpt-5.5-cyber","gemini-3.8-flash-tts","gemini-3.8-flash-lite-tts","gemini-3.1-flash-lite-image"],Xo={...qt,...Object.
29
29
  fromEntries(Qp.map(t=>[t,null]))},Xp=ti(Xo);function gn(t,e){let n=Ut(t);return n===void 0?void 0:Ka(n,Ja(e))}var ef=Object.fromEntries(Object.keys(Xo).map(t=>[t,Xo[t]===null?null:t]));function Ut(t){
30
30
  return ei(ef,Xp,t)??void 0}var tf=/^\d{4}-?\d{2}-?\d{2}$/;function st(t,e){let n=Ut(t);if(n===void 0)return;let r=t.lastIndexOf("/"),o=r>=0?t.slice(r+1):t;if(!(n!==t&&n!==o&&!tf.test(o.slice(n.length+
31
- 1))))return Ka(n,Ja(e))}var He="0.5.0-rc.1",fr="x-bvsala-version";var nf=["exact","task_equivalent","not_verified","bounded_degradation","rejected"];function rf(t){return {v:1,task:t.task,goal:t.goal,rules:t.rules??[],facts:t.facts??[],refs:t.refs??[],...t.delta?{delta:t.delta}:{},outputContract:t.outputContract}}var mr=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 i=o[0];if(/\s/.test(i)){let s=(o.
31
+ 1))))return Ka(n,Ja(e))}var He="0.5.0",fr="x-bvsala-version";var nf=["exact","task_equivalent","not_verified","bounded_degradation","rejected"];function rf(t){return {v:1,task:t.task,goal:t.goal,rules:t.rules??[],facts:t.facts??[],refs:t.refs??[],...t.delta?{delta:t.delta}:{},outputContract:t.outputContract}}var mr=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 i=o[0];if(/\s/.test(i)){let s=(o.
32
32
  match(/\n/g)??[]).length;r+=s+Math.floor(Math.max(0,o.length-1)/2);}else /[A-Za-z]/.test(i)?r+=Math.max(1,Math.ceil(o.length/4)):/\d/.test(i)?r+=Math.max(1,Math.ceil(o.length/2)):r+=1;}return Math.max(r,
33
33
  Math.ceil(e.length/4))}},St=new mr;var hr=class{estimators=new Map;modelToTokenizer=new Map;fallbackId;constructor(e=St){this.fallbackId=e.id,this.estimators.set(e.id,e);}register(e){return this.estimators.set(e.id,e),this}mapModel(e,n){
34
34
  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.
@@ -302,8 +302,8 @@ would fail it.
302
302
  | Lever | Applies to | What changes | What does not |
303
303
  |---|---|---|---|
304
304
  | Cache TTL (`cache_ttl_tuning`, on by default with PRO) | `/v1/messages` requests where a large part of the prompt is new (session start, compaction); a chained turn travels untouched | every `cache_control` with `ttl: "1h"` becomes `"5m"`, the top-level one included (a 1-hour top-level marker behind 5-minute block markers would be a 400) | a key named `cache_control` inside a tool call's `input` or a tool's `input_schema` is content and stays as it is; so does the advisor's `caching`; headers are untouched |
305
- | Per-turn effort (`BVSALA_GATEWAY_ESFUERZO=low` or `medium`) | `/v1/messages` for `claude-fable-5-1`, `claude-mythos-5-1`, `claude-opus-5-5` and `claude-opus-5`, when the level is below the model's default effort (`high`; `medium` on `claude-opus-5-5`) and the request carries no `output_config` of its own (top level or in a message); a level at or above the default leaves the request untouched | one message appended at the end of `messages`, `{"role":"system","content":[],"output_config":{"effort":…}}`, and `mid-conversation-output-config-2026-07-01` appended after your `anthropic-beta` value | your `anthropic-beta` value is kept verbatim in front; nothing earlier in `messages` moves |
306
- | Sibling model (`BVSALA_GATEWAY_MODELO_HERMANO=1`) | `/v1/messages` and `/v1/messages/count_tokens` for `claude-fable-5`, except with a forced `tool_choice` or the 1M-context beta | `model` becomes `claude-fable-5-1` | headers are untouched |
305
+ | Per-turn effort (`BVSALA_GATEWAY_EFFORT=low` or `medium`) | `/v1/messages` for `claude-fable-5-1`, `claude-mythos-5-1`, `claude-opus-5-5` and `claude-opus-5`, when the level is below the model's default effort (`high`; `medium` on `claude-opus-5-5`) and the request carries no `output_config` of its own (top level or in a message); a level at or above the default leaves the request untouched | one message appended at the end of `messages`, `{"role":"system","content":[],"output_config":{"effort":…}}`, and `mid-conversation-output-config-2026-07-01` appended after your `anthropic-beta` value | your `anthropic-beta` value is kept verbatim in front; nothing earlier in `messages` moves |
306
+ | Sibling model (`BVSALA_GATEWAY_SIBLING_MODEL=1`) | `/v1/messages` and `/v1/messages/count_tokens` for `claude-fable-5`, except with a forced `tool_choice` or the 1M-context beta | `model` becomes `claude-fable-5-1` | headers are untouched |
307
307
  | Context-editing dials (`BVSALA_GATEWAY_DIALES_ELISION=1`; measured as a net loss, off) | `/v1/messages` | your `clear_tool_uses_20250919` edit in `context_management` is replaced by ours | your other edits are kept, `clear_thinking_20251015` first; your `anthropic-beta` value is kept verbatim, with `context-management-2025-06-27` appended if it was not there |
308
308
  | Usage in Chat Completions streams (`BVSALA_GATEWAY_USAGE_INJECT=1`, off) | streaming `…/chat/completions` requests without `stream_options` of their own | `"stream_options":{"include_usage":true},` inserted as text right after the opening `{` | the rest of the body, byte for byte (nothing is re-serialized); headers are untouched; a 400 or 422 gets your original request once more |
309
309
 
@@ -47,7 +47,7 @@ codex -c openai_base_url=http://127.0.0.1:8402/v1 # ventana 2
47
47
 
48
48
  Codex añade `/responses` (`POST {base_url}/responses`, siempre `stream: true`)
49
49
  [V]. Si en el 8402 ya tienes el gateway de Anthropic para otros terminales,
50
- arranca este en otro puerto (`--puerto 8403`) y apunta ahí: un gateway habla con
50
+ arranca este en otro puerto (`--port 8403`) y apunta ahí: un gateway habla con
51
51
  un solo upstream.
52
52
 
53
53
  No lo escribas en `~/.codex/config.toml`: ahí se quedaría fijo y Codex fallaría
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivelio/savings-layer",
3
- "version": "0.5.0-rc.1",
3
+ "version": "0.5.0",
4
4
  "description": "The savings layer for your LLM bill: a library inside your code that lets through only what has to be paid for and returns a SavingsReport per call, measured with your provider's own meter and kept apart from estimates. Commercial license, paid per seat, commission only on measured savings. Your prompts never leave your infrastructure.",
5
5
  "keywords": [
6
6
  "llm",