@bivelio/savings-layer 0.3.0 → 0.3.1

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/dist/index.d.ts CHANGED
@@ -78,6 +78,26 @@ declare class SavingsLedger {
78
78
  discoveryInput: number;
79
79
  /** OUTPUT of the discovery probe. Our cost, kept out of the baseline. */
80
80
  discoveryOutput: number;
81
+ /**
82
+ * Already-priced cost of a call the model cascade made to a cheaper model
83
+ * whose answer was NOT served. Added to `actual`, never to the baseline.
84
+ */
85
+ extraCostUsd: number;
86
+ /** Input tokens of that call. Published by the pipeline, not priced here. */
87
+ cascadeInput: number;
88
+ /** Output tokens of that call. Published by the pipeline, not priced here. */
89
+ cascadeOutput: number;
90
+ /**
91
+ * Record a call to the cheap model that did not produce the served answer:
92
+ * the one discarded on an escalation, or a shadow-mode sample.
93
+ *
94
+ * The caller raises `actualLlm`, because the call happened and the report owes
95
+ * the reader that count.
96
+ */
97
+ cascadeAttempt(usage: {
98
+ inputTokens: number;
99
+ outputTokens: number;
100
+ }, cheapPrice: CostModel): void;
81
101
  /**
82
102
  * Lo que de verdad hizo la compresión, para publicarlo en
83
103
  * `SavingsReport.compression`. El campo estaba declarado en el contrato y no lo
@@ -252,21 +272,55 @@ interface SavingsToggles {
252
272
  deterministicResolvers?: boolean;
253
273
  router?: boolean;
254
274
  /**
255
- * OPT-IN. Send the provider a compacted form of your output contract — short
256
- * property keys, short enum codes — so the model spends fewer tokens saying
257
- * the same thing, and expand the reply back to YOUR shape before it is
258
- * validated, cached or returned. What you receive in `data`, `text` and
259
- * `rendered` is identical either way.
275
+ * OPT-IN, AND WHETHER IT SAVES OR COSTS DEPENDS ON THE MODEL. Read this whole
276
+ * entry before switching it on.
277
+ *
278
+ * It sends the provider a compacted form of your output contract — short
279
+ * property keys, short enum codes — and expands the reply back to YOUR shape
280
+ * before it is validated, cached or returned. What you receive in `data`,
281
+ * `text` and `rendered` is identical either way, and the layer proves that
282
+ * round trip is exact before it applies anything.
283
+ *
284
+ * WHAT LIVE A/Bs MEASURED, same triage contract, same prompts, 8 paired
285
+ * repetitions each. Input fell identically on both models, from 184 to 158
286
+ * tokens — that part is deterministic, it is the smaller contract. Output did
287
+ * not: `gpt-4o` wrote 3 tokens FEWER and the request came out 8.0% cheaper,
288
+ * while `gpt-4o-mini` wrote 16 tokens MORE and the request came out 8.2% more
289
+ * expensive. Short field names appear to give a smaller model less to go on,
290
+ * so it fills in more, and output is billed around 4x input on that family.
260
291
  *
261
- * It is applied only when the layer can prove the expansion is exact for that
262
- * schema and only when it counts as a net token win on both sides (the
263
- * compact contract itself travels as input). Otherwise your schema is sent
264
- * unchanged. Off by default because it changes what the provider sees, and
265
- * whether a model answers as WELL from a compact contract is a question only
266
- * an A/B on your own traffic can settle. Check `report.traces` for what it
267
- * decided and why.
292
+ * SO THE LEVER APPLIES ONLY WHERE THAT DELTA HAS BEEN MEASURED. It cannot be
293
+ * read off a schema — the schema was identical in both runs — so the layer
294
+ * does not guess. It uses, in order: the delta you measured and passed in
295
+ * {@link SavingsToggles.outputCodebookMeasuredOutputDeltaTokens}; the same
296
+ * delta remembered from an earlier request for that contract and model; the
297
+ * delta BiVelio has published for that model. With none of those it skips and
298
+ * says so in `report.traces`, including the exact break-even for your contract
299
+ * so you know what a measurement would have to beat.
268
300
  */
269
301
  outputCodebook?: boolean;
302
+ /**
303
+ * The output-token delta you MEASURED for the contract on this request, with
304
+ * {@link SavingsToggles.outputCodebook} on versus off, against the model you
305
+ * are sending it to. POSITIVE means the compact contract made the model write
306
+ * FEWER output tokens; negative means it wrote more.
307
+ *
308
+ * This is what lets the layer apply the codebook on a model it has not
309
+ * published a figure for, and it overrides the published one when it has —
310
+ * your traffic beats our benchmark. A negative delta switches the lever off
311
+ * for that model: the input win is capped by the size of your schema while the
312
+ * output loss grows with the answer, so no schema is large enough to buy it
313
+ * back.
314
+ *
315
+ * It belongs to ONE contract against ONE model, which is why it travels on the
316
+ * request rather than the layer config: a delta measured on your triage
317
+ * contract says nothing about your extraction contract, and one measured on
318
+ * `gpt-4o` says nothing about `gpt-4o-mini` — those two came out with opposite
319
+ * signs. Measure it with a paired A/B over the same prompts, which is what
320
+ * `benchmarks/ab-live.ts` in this package does, and measure it again when you
321
+ * change model, contract or prompt.
322
+ */
323
+ outputCodebookMeasuredOutputDeltaTokens?: number;
270
324
  /**
271
325
  * OPT-IN, and it changes the latency contract: where it applies, one request
272
326
  * becomes TWO provider calls, one after the other.
@@ -306,6 +360,249 @@ interface SavingsToggles {
306
360
  * action can be lost to a discovery step.
307
361
  */
308
362
  toolDiscovery?: boolean;
363
+ /**
364
+ * Send the request to a CHEAPER model of the same family, check the answer
365
+ * with a deterministic gate, and fall back to the model you asked for only
366
+ * when that gate rejects it. Defaults to `"off"`.
367
+ *
368
+ * It is the only lever here that changes the price of the WHOLE bill — input
369
+ * and output — instead of one axis of it. Read {@link ModelCascadeMode} for
370
+ * what each mode does, and {@link ModelCascadeReport} for the failure this
371
+ * cannot protect you from. Start at `"shadow"`.
372
+ */
373
+ modelCascade?: ModelCascadeMode;
374
+ /**
375
+ * Declare that this answer is NOT needed immediately, so the layer may ask the
376
+ * provider for a cheaper, slower processing tier. Defaults to `false`.
377
+ *
378
+ * YOU DECLARE IT; THE LAYER NEVER GUESSES. Whether a request can wait is a
379
+ * property of your business, not of the text: the same prompt is deferrable in
380
+ * a nightly enrichment job and not deferrable behind a user's cursor. There is
381
+ * no heuristic that can tell those apart, so there is no default that could be
382
+ * right.
383
+ *
384
+ * IT CHANGES YOUR LATENCY, AND THAT IS THE WHOLE POINT. Where a cheaper tier
385
+ * exists it is slower and its latency is variable, so a request on a
386
+ * user-facing path must not carry this. `report.deferred.latency` states what
387
+ * the provider documents. Nothing else about the request changes: same model,
388
+ * same prompt, same sampling controls, so the answer is the answer you would
389
+ * have got.
390
+ *
391
+ * WHAT IT IS WORTH IS READ BACK, NEVER ASSUMED. A provider short on capacity
392
+ * may serve the standard tier and bill the standard rate without saying so in
393
+ * the response body's shape. The layer therefore reads the tier the provider
394
+ * reports having SERVED and credits only that — see
395
+ * {@link DeferredExecutionReport}. On a provider with no cheaper synchronous
396
+ * tier the request is sent unchanged and the report says so.
397
+ */
398
+ deferrable?: boolean;
399
+ }
400
+ /**
401
+ * What asking for a cheaper processing tier actually got you on this request.
402
+ *
403
+ * Present whenever `savings.deferrable` is `true`, INCLUDING when it earned
404
+ * nothing — "we asked and did not get it" is the answer you need most often, and
405
+ * it is the one a report that only spoke on success would hide.
406
+ *
407
+ * ⚠️ THE FAILURE THIS EXISTS TO PREVENT. Asking for a discounted tier is not
408
+ * getting one. Providers document that a request may be served at a different
409
+ * tier from the one it asked for and billed at that tier's rate, and the
410
+ * downgrade is silent: the answer comes back looking exactly the same. A layer
411
+ * that credited the tier it REQUESTED would publish a discount your invoice does
412
+ * not carry, on every request the provider quietly downgraded.
413
+ *
414
+ * So {@link servedTier} is read from the provider's response and it is the only
415
+ * thing {@link credited} is allowed to depend on. When the provider reports no
416
+ * tier at all, nothing is credited: an absent value means "cannot be known", not
417
+ * "standard".
418
+ */
419
+ interface DeferredExecutionReport {
420
+ /** The tier the layer asked the provider for, absent if it asked for none. */
421
+ requestedTier?: string;
422
+ /**
423
+ * The tier the provider reports having SERVED, exactly as it named it. Absent
424
+ * when the provider reported none.
425
+ */
426
+ servedTier?: string;
427
+ /**
428
+ * Whether the served tier bills BELOW the standard rate card, which is the
429
+ * only condition under which a discount is claimed here.
430
+ */
431
+ credited: boolean;
432
+ /**
433
+ * Fraction of the standard list price the served tier bills — `0.5` for a tier
434
+ * documented at half price. Present only when {@link credited}.
435
+ */
436
+ discountRate?: number;
437
+ /**
438
+ * What the tier was worth on this request, in your currency unit.
439
+ *
440
+ * A COUNTERFACTUAL priced from the standard rate card: this request was not
441
+ * also run at standard price, so it is `(1 - discountRate)` applied to the
442
+ * standard-rate cost of THE ONE CALL whose {@link servedTier} was read.
443
+ *
444
+ * ⚠️ IT IS NOT `cost.actual * (1 - discountRate)`, and the difference is not
445
+ * rounding. `cost.actual` is the whole request: it can also carry a repair
446
+ * retry, a tool-discovery probe, or a model-cascade attempt that was tried and
447
+ * thrown away. No tier was read back for any of those, so none of them is
448
+ * credited here even though they very likely got the discount too. Compute the
449
+ * product yourself and you will get a LARGER number than this field — this
450
+ * field is the part that was actually observed.
451
+ *
452
+ * ⚠️ `cost.actual` prices this request at the STANDARD rate card, so when this
453
+ * field is present your invoice line will be LOWER than `cost.actual` by at
454
+ * least this amount. The bias is deliberate and it is the safe one: the layer
455
+ * under-reports the saving rather than promising one it cannot prove. The
456
+ * reason it is not folded in is {@link creditedLedger}.
457
+ */
458
+ estimatedTierSaving?: number;
459
+ /**
460
+ * Which of the four ledgers this credits: none, and that is the honest answer.
461
+ * Deferring avoids no call (ledger 1), removes no tokens (2), re-encodes
462
+ * nothing (3) and is not provider caching (4). It changes the PRICE of the
463
+ * tokens, exactly as the model cascade does, and there is no ledger for that.
464
+ *
465
+ * `cost.netSavings` therefore does not move: it is a ratio of baseline to
466
+ * actual priced from a SINGLE rate card, and a discount that scales both sides
467
+ * cannot change it. Folding this in would require a baseline priced from the
468
+ * other card — a request nobody ran.
469
+ */
470
+ creditedLedger: "none";
471
+ /**
472
+ * What the provider documents about this tier's latency, in one sentence.
473
+ *
474
+ * It is a quotation of what is published, not an estimate. Where a provider
475
+ * commits to no latency figure, this says so rather than inventing one.
476
+ */
477
+ latency: string;
478
+ /** Why it did what it did, in one sentence. Never empty. */
479
+ reason: string;
480
+ }
481
+ /**
482
+ * How much authority the model cascade has over your requests.
483
+ *
484
+ * - `"off"` (default) it neither measures nor decides. Nothing changes.
485
+ * - `"shadow"` it MEASURES and reports, and never changes your model. Every
486
+ * request still goes to the model you asked for. The report gains
487
+ * a {@link ModelCascadeReport} saying which cheaper model was
488
+ * eligible, what the arithmetic says about it, and what the swap
489
+ * would have been worth on THIS request. Costs nothing extra
490
+ * unless you also set `modelCascade.shadowSampleRate` on the
491
+ * layer, which is a separate, priced opt-in.
492
+ * - `"on"` it routes for real. Opt-in, never silent: the report always
493
+ * declares the model that served the answer, and a request served
494
+ * by a cheaper model is labelled `bounded_degradation`.
495
+ *
496
+ * `"on"` carries a gate: when a workload's measured escalation rate reaches the
497
+ * break-even, the cascade turns ITSELF off for that workload and says so. That
498
+ * gate protects your BILL. It cannot protect quality — see
499
+ * {@link ModelCascadeReport}.
500
+ */
501
+ type ModelCascadeMode = "off" | "shadow" | "on";
502
+ /**
503
+ * What the model cascade did, or would have done, on this request.
504
+ *
505
+ * Present whenever `savings.modelCascade` is not `"off"` — including when the
506
+ * request was not eligible, because "we did not route this and here is why" is
507
+ * the answer you need most often.
508
+ *
509
+ * ⚠️ READ THIS BEFORE YOU TURN THE CASCADE ON.
510
+ *
511
+ * The gate that decides whether a cheap answer is good enough is DETERMINISTIC,
512
+ * which is what makes the saving real: an LLM judge would cost the very money
513
+ * this lever exists to save. But a deterministic gate measures FORM, not
514
+ * MEANING. An answer with impeccable JSON and the wrong content passes it.
515
+ *
516
+ * That failure is invisible by construction. It lowers the escalation rate, it
517
+ * raises the reported saving, and the automatic shut-off never fires — because
518
+ * the shut-off watches the escalation rate, and the escalation rate looks
519
+ * excellent. It has the same signature as success, and it surfaces weeks later
520
+ * as customer complaints.
521
+ *
522
+ * So: `escalationRate` is a MONEY figure. Its complement is not a quality score,
523
+ * and this report deliberately does not publish one. If you need evidence about
524
+ * quality, run an A/B on your own traffic. `divergedOnSample` (from shadow
525
+ * sampling) is a hint that something is wrong, never proof that nothing is.
526
+ */
527
+ interface ModelCascadeReport {
528
+ mode: ModelCascadeMode;
529
+ /** The model the caller asked for. */
530
+ requestedModel: string;
531
+ /**
532
+ * The model that actually produced the answer. In `"shadow"` this always
533
+ * equals {@link requestedModel}; in `"on"` it is the cheap model when its
534
+ * answer was accepted, and the requested one when it was escalated.
535
+ */
536
+ servedModel: string;
537
+ /** The cheaper model this request was (or would have been) routed to. */
538
+ candidateModel?: string;
539
+ /** Whether the cascade was allowed to consider this request at all. */
540
+ eligible: boolean;
541
+ /** Why it did what it did, in one sentence. Never empty. */
542
+ reason: string;
543
+ /** What the deterministic gate is able to check here. */
544
+ verifier: "none" | "structural" | "value-constrained";
545
+ /**
546
+ * cost(cheap) / cost(strong), worst case over any token mix. The saving on
547
+ * routed traffic is `1 - r - escalationRate`.
548
+ */
549
+ costRatio?: number;
550
+ /** Escalation rate above which routing costs MORE than not routing (`1 - r`). */
551
+ breakEvenEscalationRate?: number;
552
+ /** Escalation rate above which routing stops hitting the target capture. */
553
+ targetEscalationRate?: number;
554
+ /** Opaque, stable id of the workload these rates are measured over. */
555
+ workload?: string;
556
+ /** Measured escalation rate for this workload, or absent if nothing measured yet. */
557
+ escalationRate?: number;
558
+ /** How many requests that rate is measured over. */
559
+ escalationSamples?: number;
560
+ /** Whether THIS request was escalated to the requested model after the gate rejected it. */
561
+ escalated?: boolean;
562
+ /**
563
+ * What swapping the model was worth on this request, in your currency unit.
564
+ *
565
+ * A COUNTERFACTUAL: the strong model was not run, so this is priced from the
566
+ * measured token counts at both list prices. It is deliberately NOT part of
567
+ * `cost.netSavings` — that number stays a comparison at a single price, and
568
+ * this one would inflate it with a call nobody made. Negative on an escalated
569
+ * request, which is exactly what an escalation costs you.
570
+ */
571
+ estimatedSwapSaving?: number;
572
+ /**
573
+ * Which of the four ledgers this credits: none, and that is the honest answer.
574
+ * Routing avoids no call (ledger 1), removes no tokens (2), re-encodes nothing
575
+ * (3) and is not provider caching (4). It changes the PRICE of the tokens, and
576
+ * there is no ledger for that.
577
+ */
578
+ creditedLedger: "none";
579
+ /**
580
+ * Input tokens of the cheap call whose answer was NOT served — the attempt
581
+ * discarded on an escalation, or a shadow-mode sample.
582
+ *
583
+ * They are deliberately absent from `tokens.actualInput`: another model billed
584
+ * them at another price, and that block is priced with a single cost model, so
585
+ * folding them in would price them wrong. Their MONEY is in `cost.actual`.
586
+ * These two fields are what closes the gap, so
587
+ * `tokens.actualInput * inputPer1k + tokens.actualOutput * outputPer1k` plus
588
+ * the cheap model's price applied here accounts for `cost.actual` in full.
589
+ */
590
+ cascadeInputTokens?: number;
591
+ /** Output tokens of that same unserved call. See {@link cascadeInputTokens}. */
592
+ cascadeOutputTokens?: number;
593
+ /** Whether this shadow request paid for an extra cheap call to measure the rate. */
594
+ sampled?: boolean;
595
+ /** What that extra call cost, in your currency unit. */
596
+ sampleCost?: number;
597
+ /**
598
+ * On a sampled request where BOTH answers satisfied the contract: whether they
599
+ * disagree on a hard value (a number, a boolean, a short code).
600
+ *
601
+ * A hint about false valids, and nothing more. It only covers sampled traffic,
602
+ * disagreement is not proof the cheap answer is the wrong one, and agreement
603
+ * is not proof either answer is right.
604
+ */
605
+ divergedOnSample?: boolean;
309
606
  }
310
607
  interface RequestContext {
311
608
  tenantId?: string;
@@ -395,6 +692,14 @@ type CacheOutcome = "miss" | "exact" | "semantic" | "singleflight" | "determinis
395
692
  * on top of any similarity score.
396
693
  * - `not_verified` the model was called and nothing beyond the shape of the
397
694
  * answer was checked. The honest default, and the common case.
695
+ * - `bounded_degradation`
696
+ * the answer was produced by a model CHEAPER than the one
697
+ * you asked for, and it cleared the deterministic gate for
698
+ * its output contract before being served. The bound is
699
+ * exactly what that gate checks — shape, and whatever value
700
+ * constraints your contract declares. NOT meaning: a
701
+ * well-formed answer with wrong content earns this label
702
+ * too. See {@link ModelCascadeReport}.
398
703
  * - `rejected` what was served does not satisfy its output contract, and
399
704
  * a repair either failed or would have been a wasted call.
400
705
  * The answer is still returned: this is the layer's verdict
@@ -405,10 +710,19 @@ type CacheOutcome = "miss" | "exact" | "semantic" | "singleflight" | "determinis
405
710
  * the day they can demonstrate it. Read this as an OPEN set — keep a default
406
711
  * branch, exactly as with a licence status.
407
712
  */
408
- declare const QUALITY_STATUSES: readonly ["exact", "task_equivalent", "not_verified", "rejected"];
713
+ declare const QUALITY_STATUSES: readonly ["exact", "task_equivalent", "not_verified", "bounded_degradation", "rejected"];
409
714
  type QualityStatus = (typeof QUALITY_STATUSES)[number];
410
715
  interface SavingsReport {
411
716
  requestId: string;
717
+ /**
718
+ * The model that produced this answer.
719
+ *
720
+ * Equal to the model you asked for, EXCEPT when `savings.modelCascade` is
721
+ * `"on"` and a cheaper model's answer cleared the deterministic gate — then
722
+ * this names the cheaper model, because that is what ran and what your bill
723
+ * will show. `report.modelCascade.requestedModel` always holds what you asked
724
+ * for. Changing your model is never silent.
725
+ */
412
726
  model: string;
413
727
  calls: {
414
728
  baselineLlm: number;
@@ -480,6 +794,17 @@ interface SavingsReport {
480
794
  * the `exact` label.
481
795
  */
482
796
  qualityStatus?: QualityStatus;
797
+ /**
798
+ * What the model cascade did, or would have done. Present whenever
799
+ * `savings.modelCascade` is not `"off"`. Read its docstring before turning the
800
+ * cascade on: it documents a failure mode no gate in this layer can catch.
801
+ */
802
+ modelCascade?: ModelCascadeReport;
803
+ /**
804
+ * What asking for a cheaper processing tier got you. Present whenever
805
+ * `savings.deferrable` is `true`, including when it got you nothing.
806
+ */
807
+ deferred?: DeferredExecutionReport;
483
808
  quality: {
484
809
  taskPassed?: boolean;
485
810
  /**
@@ -524,10 +849,14 @@ interface SavingsReport {
524
849
  * Structural license gate the pipeline consults before enabling a managed
525
850
  * feature (semantic cache, learned thresholds, long-context compression, …).
526
851
  *
527
- * It is intentionally a bare shape — the SDK never imports the license package.
528
- * `@bivelio/license-client`'s `createLicenseGate()` returns a compatible object,
529
- * but any implementation of this interface works. When no gate is configured the
530
- * pipeline behaves as an ungated self-managed deployment (backwards compatible).
852
+ * It is intentionally a bare shape, so any object with these methods works and
853
+ * the SDK never has to import a licensing package.
854
+ *
855
+ * There is no ungated mode: with no gate and no `licenseKey`, `generate()`
856
+ * throws {@link LicenseError} on entry. In the published npm package an injected
857
+ * gate is ignored entirely — a `licenseKey` verified against BiVelio's embedded
858
+ * public key is the only accepted license there, and this interface is what that
859
+ * key is turned into internally.
531
860
  */
532
861
  interface LicenseGate {
533
862
  isEntitled(feature: string): boolean;
@@ -783,6 +1112,14 @@ declare function normalizeText(text: string): string;
783
1112
  declare function sha256(input: string): string;
784
1113
  /** Stable JSON stringify with sorted keys so hashes are order-independent. */
785
1114
  declare function stableStringify(value: unknown): string;
1115
+ /**
1116
+ * The version-and-size-independent family of a model id.
1117
+ *
1118
+ * Exported because the cache key is now built for the model that will actually
1119
+ * ANSWER, which is not always the one the caller named. Deriving the family a
1120
+ * second time somewhere else is how two rulers start to disagree.
1121
+ */
1122
+ declare function modelFamilyOf(model: string): string;
786
1123
  declare function canonicalize(request: SavingsRequest): CanonicalRequest;
787
1124
 
788
1125
  /**
@@ -792,8 +1129,21 @@ declare function canonicalize(request: SavingsRequest): CanonicalRequest;
792
1129
  * text but differ in tenant, permission scope, model version, system prompt,
793
1130
  * schema, toolset, locale, sampling, knowledge epoch or safety policy — any of
794
1131
  * which can change the correct answer. The exact key includes all of them.
1132
+ *
1133
+ * `servedModel` CARRIES THE MODEL THAT WILL ACTUALLY ANSWER, not the one the
1134
+ * caller named, and that distinction is what stops the model cascade from
1135
+ * poisoning the cache. Routing to a cheap model and storing its answer under the
1136
+ * STRONG model's key would hand that answer to the next request — one with the
1137
+ * cascade off, or from another process entirely — while the report claims the
1138
+ * requested model served it. That is a silent model swap, deferred, which is the
1139
+ * worst kind of silent model swap.
1140
+ *
1141
+ * `modelFamily` is derived from whichever id is used, for the same reason: it is
1142
+ * part of the identity of whoever answers.
795
1143
  */
796
- declare function buildExactKey(c: CanonicalRequest): string;
1144
+ declare function buildExactKey(c: CanonicalRequest, opciones?: {
1145
+ servedModel?: string;
1146
+ }): string;
797
1147
  /**
798
1148
  * In-flight (singleflight) key. Coalesces concurrent, equivalent requests onto
799
1149
  * a single execution. It can be slightly looser than the exact key (it does not
@@ -804,8 +1154,37 @@ declare function buildExactKey(c: CanonicalRequest): string;
804
1154
  * two concurrent calls that differ only in `schemaVersion` are different
805
1155
  * requests and must never coalesce. All three doors — exact key, in-flight key
806
1156
  * and semantic filter — key on the same identity, by design.
807
- */
808
- declare function buildInflightKey(c: CanonicalRequest): string;
1157
+ *
1158
+ * `servedModel` CARRIES THE MODEL THAT WILL ACTUALLY ANSWER, for the same reason
1159
+ * {@link buildExactKey} does, and this door is the one that leaks FIRST. The
1160
+ * exact key protects the answer AFTER it is stored; this one protects it while
1161
+ * it is still being written, and a follower does not re-run anything — it is
1162
+ * handed the leader's text verbatim.
1163
+ *
1164
+ * The leak it closes, measured: two concurrent requests for the same text, one
1165
+ * with `savings.modelCascade: "on"` and one with the cascade OFF, coalesced onto
1166
+ * one execution. The routed leader called the cheap model, and the caller who
1167
+ * never opted in — whose own decision was "do not route" — was served the cheap
1168
+ * model's answer. Same shape inside a single uniformly-configured deployment:
1169
+ * the automatic shut-off can latch between two concurrent requests, so one is
1170
+ * routed and the other is not, and coalescing hands one of them the other's
1171
+ * model. Whoever the layer decided must go to the strong model has to actually
1172
+ * go there.
1173
+ *
1174
+ * `deferrable` CLOSES THE SAME HOLE ON THE LATENCY AXIS, which is why it belongs
1175
+ * here and NOT in {@link buildExactKey}. The two doors do not risk the same
1176
+ * thing: the exact key hands out an answer that is ALREADY WRITTEN, where the
1177
+ * processing tier only picked a queue and changes neither the text nor the wait
1178
+ * of whoever receives it. This one binds a follower to a call that HAS NOT COME
1179
+ * BACK YET, so it imposes the leader's wait on them.
1180
+ *
1181
+ * A caller who never set `savings.deferrable` must not be made to wait for one
1182
+ * who did.
1183
+ */
1184
+ declare function buildInflightKey(c: CanonicalRequest, opciones?: {
1185
+ servedModel?: string;
1186
+ deferrable?: boolean;
1187
+ }): string;
809
1188
 
810
1189
  /**
811
1190
  * Policy engine: risk and freshness.
@@ -1604,6 +1983,19 @@ interface ProviderRequest {
1604
1983
  * carry tenant or user data, because it does not isolate anything.
1605
1984
  */
1606
1985
  promptCacheKey?: string;
1986
+ /**
1987
+ * The caller declares this answer is not needed immediately, so the adapter
1988
+ * may ask for a cheaper, slower processing tier.
1989
+ *
1990
+ * It is DECLARED, never inferred: whether a request can wait is a property of
1991
+ * the business, not of the text. An adapter whose provider has no cheaper
1992
+ * synchronous tier leaves the request unchanged.
1993
+ *
1994
+ * Asking is not getting. A provider may serve a downgraded tier and bill it at
1995
+ * the standard rate, so the discount is only ever read back from
1996
+ * {@link ProviderResponse.servedServiceTier}.
1997
+ */
1998
+ deferrable?: boolean;
1607
1999
  }
1608
2000
  interface ProviderUsage {
1609
2001
  inputTokens: number;
@@ -1655,6 +2047,30 @@ interface ProviderResponse {
1655
2047
  * all the way to the caller.
1656
2048
  */
1657
2049
  toolCalls?: ToolCallRequest[];
2050
+ /**
2051
+ * The processing tier the provider reports having SERVED this request, exactly
2052
+ * as it named it (OpenAI `service_tier`, Anthropic `usage.service_tier`).
2053
+ *
2054
+ * This is the only field a discount may be credited from, and the reason is
2055
+ * that it is not always what was asked for: a provider short on capacity may
2056
+ * serve the standard tier and bill the standard rate while the request said
2057
+ * otherwise. Crediting the REQUESTED tier would publish a saving the invoice
2058
+ * does not carry.
2059
+ *
2060
+ * Absent means the provider said nothing, which is NOT the same as "standard":
2061
+ * it means the tier cannot be known, so nothing is credited.
2062
+ */
2063
+ servedServiceTier?: string;
2064
+ /**
2065
+ * The processing tier this adapter ASKED the provider for, when it asked for
2066
+ * one. Absent means it asked for nothing — which is the honest answer for a
2067
+ * provider that has no cheaper synchronous tier to ask for.
2068
+ *
2069
+ * Reported by the adapter rather than assumed by the caller: only the adapter
2070
+ * knows its provider's vocabulary, and a caller that hardcoded a tier name
2071
+ * would be describing a request some other adapter never made.
2072
+ */
2073
+ requestedServiceTier?: string;
1658
2074
  raw?: unknown;
1659
2075
  }
1660
2076
  interface Provider {
@@ -1670,6 +2086,22 @@ interface Provider {
1670
2086
  * demonstrating the constrained-decoding contract end to end. Token usage is
1671
2087
  * derived from the heuristic estimator so SavingsReport numbers are meaningful
1672
2088
  * in examples and tests.
2089
+ *
2090
+ * ⚠️ WHAT THIS PROVIDER CANNOT MEASURE, and it has already cost money once.
2091
+ * It fabricates the answer FROM THE SCHEMA it receives, so a smaller schema
2092
+ * always produces a smaller answer. Any lever that shrinks the contract sent to
2093
+ * the provider therefore appears to shrink the OUTPUT too — automatically, on
2094
+ * every run, whatever model id is being simulated. That is a property of this
2095
+ * file, not of any model.
2096
+ *
2097
+ * The output codebook is exactly that shape of lever, and against this provider
2098
+ * the benchmark reported it saving 24.8%. Measured live it saves 8.0% on
2099
+ * `gpt-4o` and COSTS 8.2% on `gpt-4o-mini`, because a real model decides how
2100
+ * much to WRITE and a smaller model writes more when the field names carry less
2101
+ * meaning. So: never take an output-token saving measured here as evidence of
2102
+ * anything. Prove those with the paired A/B in `benchmarks/ab-live.ts`, and
2103
+ * write tests for output-side effects against a provider that models the
2104
+ * behaviour you are claiming — see `test/codebook-de-salida.test.ts`.
1673
2105
  */
1674
2106
  interface MockProviderOptions {
1675
2107
  /** Fixed text to return when no schema is present. */
@@ -1833,7 +2265,19 @@ declare class TraceCollector {
1833
2265
  * unlicensed, rather than silently serving requests for free.
1834
2266
  */
1835
2267
  declare class LicenseError extends Error {
1836
- /** Resolved license status, when known: expired | invalid | revoked | none. */
2268
+ /**
2269
+ * Resolved license status — the stable handle for branching on the cause,
2270
+ * since the message text is rewritten whenever it can be made clearer:
2271
+ *
2272
+ * - `none` — nothing was configured (no `licenseKey`, no gate).
2273
+ * - `invalid` — a key was configured but does not verify.
2274
+ * - `expired` / `grace` — it verifies, but the term is over.
2275
+ * - `revoked` — cancelled by the control plane.
2276
+ * - `stale` — paid and current, but unconfirmed: a connectivity problem.
2277
+ *
2278
+ * The same value is repeated at the end of the message (`license status: X`)
2279
+ * because an uncaught error prints nothing else.
2280
+ */
1837
2281
  readonly status?: string;
1838
2282
  constructor(message: string, status?: string);
1839
2283
  }
@@ -1842,10 +2286,10 @@ declare class LicenseError extends Error {
1842
2286
  * Offline license verification for the paid SDK.
1843
2287
  *
1844
2288
  * A license is a compact signed token `base64url(header).base64url(payload).
1845
- * base64url(sig)` (the exact format the licensing service mints in
1846
- * `@bivelio/license-client`'s `signLicense`). We verify the Ed25519 signature
1847
- * against BiVelio's public key, decode the payload, and apply the same grace
1848
- * policy the license-client uses. Verification is **browser-safe**: it uses
2289
+ * base64url(sig)`, the exact format BiVelio's licensing service mints. We verify
2290
+ * the Ed25519 signature against BiVelio's public key, decode the payload, and
2291
+ * apply the same grace policy the issuer applies. Verification is
2292
+ * **browser-safe**: it uses
1849
2293
  * `@noble/curves` (pure JS) + Web primitives (`atob`, `TextEncoder`) rather than
1850
2294
  * `node:crypto`, so the shared entry point still compiles into the browser
1851
2295
  * demo bundle. The private signing key never leaves the control plane; only the
@@ -1943,8 +2387,8 @@ declare function deriveStatus(payload: {
1943
2387
  grace_days: number;
1944
2388
  }, nowSeconds: number, revoked?: ReadonlySet<string>): LicenseStatus;
1945
2389
  /**
1946
- * Verify + apply the grace policy → a resolved status. Mirrors
1947
- * `@bivelio/license-client`'s `LicenseClient.resolve`. Bad signature / malformed
2390
+ * Verify + apply the grace policy → a resolved status. Mirrors what the issuing
2391
+ * control plane resolves for the same token. Bad signature / malformed
1948
2392
  * → invalid; otherwise the status comes from {@link deriveStatus}.
1949
2393
  */
1950
2394
  declare function resolveLicense(token: string, publicKey: string | LicenseKeyset, opts?: {
@@ -2326,6 +2770,11 @@ interface SavingsLayerConfig {
2326
2770
  compressionEconomics?: CompressionEconomics;
2327
2771
  retriever?: Retriever;
2328
2772
  reranker?: Reranker;
2773
+ /**
2774
+ * Tuning for the model cascade (`savings.modelCascade`). All optional; the
2775
+ * defaults measure and never spend.
2776
+ */
2777
+ modelCascade?: ModelCascadeSettings;
2329
2778
  /**
2330
2779
  * Signed BiVelio license token (from your paid subscription). This is the
2331
2780
  * primary way to license the SDK: `createSavingsLayer` verifies it offline
@@ -2343,10 +2792,11 @@ interface SavingsLayerConfig {
2343
2792
  */
2344
2793
  licensePublicKey?: string | LicenseKeyset;
2345
2794
  /**
2346
- * Pre-built license gate. Usually you pass {@link licenseKey} instead and let
2347
- * the SDK construct this. Advanced/self-managed callers can inject their own
2348
- * gate (e.g. `@bivelio/license-client`'s `createLicenseGate()`). When both are
2349
- * given, `licenseGate` wins.
2795
+ * Pre-built license gate. **Ignored in the published npm package**, where a
2796
+ * {@link licenseKey} verified against the embedded BiVelio public key is the
2797
+ * only accepted license — passing a gate there logs a warning and changes
2798
+ * nothing. It exists for builds from source (this repo's own tests and
2799
+ * benchmarks), where it takes precedence over {@link licenseKey}.
2350
2800
  */
2351
2801
  licenseGate?: LicenseGate;
2352
2802
  /**
@@ -2492,6 +2942,58 @@ interface SavingsLayerConfig {
2492
2942
  /** Injectable id generator for deterministic tests. */
2493
2943
  generateId?: () => string;
2494
2944
  }
2945
+ /**
2946
+ * Tuning for the model cascade. Everything here is optional, and the defaults
2947
+ * cost nothing: with no settings at all, `modelCascade: "shadow"` measures what
2948
+ * it can measure for free and makes no extra provider call.
2949
+ */
2950
+ interface ModelCascadeSettings {
2951
+ /**
2952
+ * Your own strong → cheap pairs, replacing the built-in table.
2953
+ *
2954
+ * Keys and values are model ids WITHOUT a vendor prefix (`"gpt-4o"`, not
2955
+ * `"openai/gpt-4o"`); the caller's prefix is carried over automatically, and a
2956
+ * dated snapshot inherits from its base id. A pair whose economics do not work
2957
+ * out at your prices is refused with a reason in the report — declaring it here
2958
+ * does not override the arithmetic.
2959
+ */
2960
+ pairs?: Record<string, string>;
2961
+ /**
2962
+ * How many requests of a workload are routed before the automatic shut-off
2963
+ * judges the measured escalation rate (default 20).
2964
+ *
2965
+ * The rate is only observable on requests that were actually routed, so there
2966
+ * is no way to know without spending a bounded amount to find out. Worst case
2967
+ * (every one of them escalates) that exploration costs `n x r` requests'
2968
+ * worth of extra spend, once per workload per process — with a ratio of 0.06
2969
+ * and the default, about 1.2 requests.
2970
+ */
2971
+ exploration?: number;
2972
+ /**
2973
+ * PRICED OPT-IN, default 0 (off). Fraction of `"shadow"` requests on which the
2974
+ * layer also calls the cheap model, purely to measure.
2975
+ *
2976
+ * Shadow mode is free precisely because it does not run the cheap model: it
2977
+ * reports eligibility, the arithmetic, and what the swap would have been worth.
2978
+ * What it cannot know for free is the one number that decides everything — how
2979
+ * often the deterministic gate would have REJECTED the cheap answer. That
2980
+ * answer does not exist until the cheap model writes one.
2981
+ *
2982
+ * So this buys it, and the price is exact: each sampled request costs one extra
2983
+ * cheap call, i.e. `r` of what the request already cost — about 6% for
2984
+ * gpt-4o → gpt-4o-mini. At `0.05` that is roughly 0.3% on top of your bill, and
2985
+ * every sampled request reports its own `modelCascade.sampleCost`. It also
2986
+ * DOUBLES that request's provider latency, because the extra call is sequential.
2987
+ *
2988
+ * The sampled request's tokens count as spend and are held out of the baseline,
2989
+ * exactly like the repair retry: measurement must never pay for itself.
2990
+ *
2991
+ * Which requests get sampled is derived from the request's own cache key, so it
2992
+ * is deterministic and stable — the same request is always sampled or always
2993
+ * not, and no `Math.random` decides how your money is spent.
2994
+ */
2995
+ shadowSampleRate?: number;
2996
+ }
2495
2997
  declare const DEFAULT_COST_MODEL: CostModel;
2496
2998
 
2497
2999
  /** Fully-resolved, non-optional dependency set the pipeline runs against. */
@@ -2531,9 +3033,15 @@ interface SavingsLayer {
2531
3033
  * cache, a vector-backed semantic cache, a real tokenizer, a compressor sidecar,
2532
3034
  * XGrammar/llguidance backends, etc.).
2533
3035
  *
3036
+ * A `licenseKey` is not optional in practice: `generate()` is fail-closed and
3037
+ * throws {@link LicenseError} without one, so it belongs in the smallest example
3038
+ * there is.
3039
+ *
2534
3040
  * @example
2535
- * const layer = createSavingsLayer({ provider: createMockProvider() });
2536
- * const { data, report } = await layer.generate({
3041
+ * // `licenseKey` is the signed token from savings.bivelio.com/dashboard/install,
3042
+ * // read from wherever you keep secrets (BIVELIO_LICENSE_KEY in the README).
3043
+ * const layer = createSavingsLayer({ provider, licenseKey });
3044
+ * const { text, report } = await layer.generate({
2537
3045
  * model: "openai/gpt-4o-mini",
2538
3046
  * messages: [{ role: "user", content: "What is 2 + 2 * 3?" }],
2539
3047
  * });
@@ -2567,6 +3075,6 @@ declare function createSavingsLayer(config: Omit<SavingsLayerConfig, "tokenizer"
2567
3075
  * constant to package.json so the two can never drift.
2568
3076
  */
2569
3077
 
2570
- declare const BV_SALA_VERSION = "0.3.0";
3078
+ declare const BV_SALA_VERSION = "0.3.1";
2571
3079
 
2572
- 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, 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 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, UsageEmitter, type UsageEmitterOptions, type ValidationError, type ValidationResult, adaptiveK, arithmeticResolver, buildExactKey, buildInflightKey, canonicalize, compactRowsSerializer, computeNextDelay, cosine, createCompactRowsSerializer, createDefaultResolvers, createHandoff, createMockProvider, createSavingsLayer, csvSerializer, decodeCompactRows, dedupe, defaultTokenizerRegistry, deriveSeatId, deriveStatus, detectCi, emptyState, encodeCompactRows, evalArithmetic, evaluatePolicy, executeRetrievalPlan, gateFromKey, heuristicEstimator, isKeyset, isStrictCompatible, jsonCompactSerializer, keyTokensMatch, liveGateFromKey, mergeDelta, noopCompressor, normalizeText, precioDeLista, projectContext, providerNativeBackend, randomId, registerExactTokenizer, registrarTokenizadorExacto, renderState, resolveConstraintBackend, resolveDependencies, resolveLicense, sha256, shouldCompress, stableStringify, uniformColumns, validateAgainstSchema, verifyCompactSignature, verifyLicenseToken, verifyRevocationList, withProtectedSpans };
3080
+ 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, 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, UsageEmitter, type UsageEmitterOptions, type ValidationError, type ValidationResult, adaptiveK, arithmeticResolver, buildExactKey, buildInflightKey, canonicalize, compactRowsSerializer, computeNextDelay, cosine, createCompactRowsSerializer, createDefaultResolvers, createHandoff, createMockProvider, createSavingsLayer, csvSerializer, decodeCompactRows, dedupe, defaultTokenizerRegistry, deriveSeatId, deriveStatus, detectCi, emptyState, encodeCompactRows, evalArithmetic, evaluatePolicy, executeRetrievalPlan, gateFromKey, heuristicEstimator, isKeyset, isStrictCompatible, jsonCompactSerializer, keyTokensMatch, liveGateFromKey, mergeDelta, modelFamilyOf, noopCompressor, normalizeText, precioDeLista, projectContext, providerNativeBackend, randomId, registerExactTokenizer, registrarTokenizadorExacto, renderState, resolveConstraintBackend, resolveDependencies, resolveLicense, sha256, shouldCompress, stableStringify, uniformColumns, validateAgainstSchema, verifyCompactSignature, verifyLicenseToken, verifyRevocationList, withProtectedSpans };