@bivelio/savings-layer 0.3.0 → 0.4.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 +4 -2
- package/dist/index.d.ts +663 -37
- package/dist/index.js +23 -23
- package/package.json +4 -2
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
|
|
256
|
-
*
|
|
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.
|
|
260
277
|
*
|
|
261
|
-
* It
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
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.
|
|
291
|
+
*
|
|
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
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
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
|
|
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
|
-
|
|
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.
|
|
@@ -969,7 +1348,21 @@ interface SemanticHit {
|
|
|
969
1348
|
ageMs: number;
|
|
970
1349
|
}
|
|
971
1350
|
interface SemanticCacheOptions {
|
|
972
|
-
|
|
1351
|
+
/**
|
|
1352
|
+
* How text becomes a vector. OPTIONAL: omit it and {@link embebedorLocal} is
|
|
1353
|
+
* used — no network, no API key, no per-call cost.
|
|
1354
|
+
*
|
|
1355
|
+
* It used to be required, and the package shipped NO implementation, so this
|
|
1356
|
+
* cache could not be constructed without bringing your own embedding provider.
|
|
1357
|
+
* That is why the benchmark reported it as unmeasurable run after run: not that
|
|
1358
|
+
* it saved little, but that it never got to run at all.
|
|
1359
|
+
*
|
|
1360
|
+
* Pass your own for true semantics; the local one only sees LEXICAL closeness.
|
|
1361
|
+
* Either way the deterministic guard below still rejects a candidate whose
|
|
1362
|
+
* numbers, negation or entities differ, so a weaker vector can only cost you a
|
|
1363
|
+
* cache miss — never a wrong answer.
|
|
1364
|
+
*/
|
|
1365
|
+
embed?: EmbeddingFn;
|
|
973
1366
|
verify?: SemanticVerifier;
|
|
974
1367
|
/** Cosine similarity floor for a candidate to be considered at all. */
|
|
975
1368
|
similarityThreshold?: number;
|
|
@@ -997,7 +1390,7 @@ declare class InMemorySemanticCache {
|
|
|
997
1390
|
private readonly ttlMs;
|
|
998
1391
|
private readonly maxRecords;
|
|
999
1392
|
private readonly now;
|
|
1000
|
-
constructor(opts
|
|
1393
|
+
constructor(opts?: SemanticCacheOptions);
|
|
1001
1394
|
/**
|
|
1002
1395
|
* Find a safe hit for a canonical request, or null. Metadata filters run
|
|
1003
1396
|
* FIRST and hard — a candidate from another tenant, scope, or knowledge epoch
|
|
@@ -1010,6 +1403,63 @@ declare class InMemorySemanticCache {
|
|
|
1010
1403
|
get size(): number;
|
|
1011
1404
|
}
|
|
1012
1405
|
|
|
1406
|
+
/**
|
|
1407
|
+
* A LOCAL EMBEDDING FUNCTION — no network, no API key, no per-call cost.
|
|
1408
|
+
*
|
|
1409
|
+
* The semantic cache needs a way to compare meaning. Wiring it to a hosted
|
|
1410
|
+
* embedding endpoint is the obvious answer and a bad default: it makes every
|
|
1411
|
+
* miss cost a network round-trip and money, and it forces a provider choice onto
|
|
1412
|
+
* a customer who never asked for one. So the cache shipped with no embedder at
|
|
1413
|
+
* all, which means it never matched anything.
|
|
1414
|
+
*
|
|
1415
|
+
* This is the third option: a deterministic bag-of-features vector computed in
|
|
1416
|
+
* microseconds from the text itself. It captures LEXICAL closeness — the same
|
|
1417
|
+
* question asked with different filler words — which is the overwhelming majority
|
|
1418
|
+
* of real repeat traffic. It does NOT capture deep semantics: "what is a CDN"
|
|
1419
|
+
* and "explain content delivery networks" share no tokens and will not match.
|
|
1420
|
+
*
|
|
1421
|
+
* WHY THAT IS SAFE, AND THIS IS THE PART THAT MATTERS. A semantic cache's one
|
|
1422
|
+
* unforgivable failure is confidently answering a NEARBY BUT DIFFERENT question.
|
|
1423
|
+
* This vector never gets the chance: the cache runs a deterministic precision
|
|
1424
|
+
* guard beneath the embedding — matching numbers and dates, negation parity and a
|
|
1425
|
+
* content-overlap floor — and every isolation field (tenant, principal, model,
|
|
1426
|
+
* schema, attached data) is a hard filter applied before similarity is scored.
|
|
1427
|
+
* A weak embedding can only cause a MISS, which costs one real call. It cannot
|
|
1428
|
+
* produce a wrong answer.
|
|
1429
|
+
*
|
|
1430
|
+
* Pass your own `embed` to {@link InMemorySemanticCache} to get true semantics.
|
|
1431
|
+
* This one exists so that the feature is worth something with zero setup.
|
|
1432
|
+
*/
|
|
1433
|
+
/**
|
|
1434
|
+
* The vector for a piece of text.
|
|
1435
|
+
*
|
|
1436
|
+
* Features are unigrams plus adjacent bigrams: bigrams are what keep word ORDER
|
|
1437
|
+
* from being free. Without them "does A cause B" and "does B cause A" are the
|
|
1438
|
+
* same point in space.
|
|
1439
|
+
*
|
|
1440
|
+
* Term weight is sublinear (`1 + log tf`) so a word repeated forty times does not
|
|
1441
|
+
* dominate the direction of the vector, and the result is L2-normalised so cosine
|
|
1442
|
+
* similarity is a plain dot product.
|
|
1443
|
+
*/
|
|
1444
|
+
declare function vectorLocal(texto: string): number[];
|
|
1445
|
+
/**
|
|
1446
|
+
* The default embedding function for {@link InMemorySemanticCache}.
|
|
1447
|
+
*
|
|
1448
|
+
* Synchronous by design — it returns the array, not a promise — so a cache lookup
|
|
1449
|
+
* adds no await and no latency to the request path.
|
|
1450
|
+
*/
|
|
1451
|
+
declare function embebedorLocal(texto: string): number[];
|
|
1452
|
+
/**
|
|
1453
|
+
* The similarity floor this embedder is calibrated for.
|
|
1454
|
+
*
|
|
1455
|
+
* A lexical vector sits lower than a sentence-transformer on genuine paraphrases,
|
|
1456
|
+
* so the 0.9 default that suits a hosted embedder would reject almost everything
|
|
1457
|
+
* here. This value was measured on paraphrase pairs against near-miss pairs, not
|
|
1458
|
+
* guessed — see `test/embebedor-local.test.ts`, which fails if the two
|
|
1459
|
+
* populations stop being separable.
|
|
1460
|
+
*/
|
|
1461
|
+
declare const UMBRAL_LOCAL = 0.72;
|
|
1462
|
+
|
|
1013
1463
|
/**
|
|
1014
1464
|
* Deterministic resolvers.
|
|
1015
1465
|
*
|
|
@@ -1604,6 +2054,19 @@ interface ProviderRequest {
|
|
|
1604
2054
|
* carry tenant or user data, because it does not isolate anything.
|
|
1605
2055
|
*/
|
|
1606
2056
|
promptCacheKey?: string;
|
|
2057
|
+
/**
|
|
2058
|
+
* The caller declares this answer is not needed immediately, so the adapter
|
|
2059
|
+
* may ask for a cheaper, slower processing tier.
|
|
2060
|
+
*
|
|
2061
|
+
* It is DECLARED, never inferred: whether a request can wait is a property of
|
|
2062
|
+
* the business, not of the text. An adapter whose provider has no cheaper
|
|
2063
|
+
* synchronous tier leaves the request unchanged.
|
|
2064
|
+
*
|
|
2065
|
+
* Asking is not getting. A provider may serve a downgraded tier and bill it at
|
|
2066
|
+
* the standard rate, so the discount is only ever read back from
|
|
2067
|
+
* {@link ProviderResponse.servedServiceTier}.
|
|
2068
|
+
*/
|
|
2069
|
+
deferrable?: boolean;
|
|
1607
2070
|
}
|
|
1608
2071
|
interface ProviderUsage {
|
|
1609
2072
|
inputTokens: number;
|
|
@@ -1655,6 +2118,30 @@ interface ProviderResponse {
|
|
|
1655
2118
|
* all the way to the caller.
|
|
1656
2119
|
*/
|
|
1657
2120
|
toolCalls?: ToolCallRequest[];
|
|
2121
|
+
/**
|
|
2122
|
+
* The processing tier the provider reports having SERVED this request, exactly
|
|
2123
|
+
* as it named it (OpenAI `service_tier`, Anthropic `usage.service_tier`).
|
|
2124
|
+
*
|
|
2125
|
+
* This is the only field a discount may be credited from, and the reason is
|
|
2126
|
+
* that it is not always what was asked for: a provider short on capacity may
|
|
2127
|
+
* serve the standard tier and bill the standard rate while the request said
|
|
2128
|
+
* otherwise. Crediting the REQUESTED tier would publish a saving the invoice
|
|
2129
|
+
* does not carry.
|
|
2130
|
+
*
|
|
2131
|
+
* Absent means the provider said nothing, which is NOT the same as "standard":
|
|
2132
|
+
* it means the tier cannot be known, so nothing is credited.
|
|
2133
|
+
*/
|
|
2134
|
+
servedServiceTier?: string;
|
|
2135
|
+
/**
|
|
2136
|
+
* The processing tier this adapter ASKED the provider for, when it asked for
|
|
2137
|
+
* one. Absent means it asked for nothing — which is the honest answer for a
|
|
2138
|
+
* provider that has no cheaper synchronous tier to ask for.
|
|
2139
|
+
*
|
|
2140
|
+
* Reported by the adapter rather than assumed by the caller: only the adapter
|
|
2141
|
+
* knows its provider's vocabulary, and a caller that hardcoded a tier name
|
|
2142
|
+
* would be describing a request some other adapter never made.
|
|
2143
|
+
*/
|
|
2144
|
+
requestedServiceTier?: string;
|
|
1658
2145
|
raw?: unknown;
|
|
1659
2146
|
}
|
|
1660
2147
|
interface Provider {
|
|
@@ -1670,6 +2157,22 @@ interface Provider {
|
|
|
1670
2157
|
* demonstrating the constrained-decoding contract end to end. Token usage is
|
|
1671
2158
|
* derived from the heuristic estimator so SavingsReport numbers are meaningful
|
|
1672
2159
|
* in examples and tests.
|
|
2160
|
+
*
|
|
2161
|
+
* ⚠️ WHAT THIS PROVIDER CANNOT MEASURE, and it has already cost money once.
|
|
2162
|
+
* It fabricates the answer FROM THE SCHEMA it receives, so a smaller schema
|
|
2163
|
+
* always produces a smaller answer. Any lever that shrinks the contract sent to
|
|
2164
|
+
* the provider therefore appears to shrink the OUTPUT too — automatically, on
|
|
2165
|
+
* every run, whatever model id is being simulated. That is a property of this
|
|
2166
|
+
* file, not of any model.
|
|
2167
|
+
*
|
|
2168
|
+
* The output codebook is exactly that shape of lever, and against this provider
|
|
2169
|
+
* the benchmark reported it saving 24.8%. Measured live it saves 8.0% on
|
|
2170
|
+
* `gpt-4o` and COSTS 8.2% on `gpt-4o-mini`, because a real model decides how
|
|
2171
|
+
* much to WRITE and a smaller model writes more when the field names carry less
|
|
2172
|
+
* meaning. So: never take an output-token saving measured here as evidence of
|
|
2173
|
+
* anything. Prove those with the paired A/B in `benchmarks/ab-live.ts`, and
|
|
2174
|
+
* write tests for output-side effects against a provider that models the
|
|
2175
|
+
* behaviour you are claiming — see `test/codebook-de-salida.test.ts`.
|
|
1673
2176
|
*/
|
|
1674
2177
|
interface MockProviderOptions {
|
|
1675
2178
|
/** Fixed text to return when no schema is present. */
|
|
@@ -1799,6 +2302,18 @@ interface AnthropicOptions {
|
|
|
1799
2302
|
* pointless: the extra breakpoints are rejected by the API, not ignored.
|
|
1800
2303
|
*/
|
|
1801
2304
|
maxCacheBreakpoints?: number;
|
|
2305
|
+
/**
|
|
2306
|
+
* Whether a request carrying BOTH the caller's tools and an output contract
|
|
2307
|
+
* may send `tool_choice: {type: "any"}`, which obliges the model to answer
|
|
2308
|
+
* through SOME tool — its pick — instead of writing prose.
|
|
2309
|
+
*
|
|
2310
|
+
* Defaults to `true`, and the default is the point: without it this exact
|
|
2311
|
+
* combination measured 62% MORE expensive than sending no layer at all, every
|
|
2312
|
+
* single time, because prose forces a second full call to repair. Turning it
|
|
2313
|
+
* off restores the old behaviour for a caller who genuinely needs a plain-text
|
|
2314
|
+
* turn to be reachable while a contract is attached.
|
|
2315
|
+
*/
|
|
2316
|
+
forceAnswerThroughTool?: boolean;
|
|
1802
2317
|
}
|
|
1803
2318
|
declare class AnthropicProvider implements Provider {
|
|
1804
2319
|
readonly id: string;
|
|
@@ -1833,7 +2348,19 @@ declare class TraceCollector {
|
|
|
1833
2348
|
* unlicensed, rather than silently serving requests for free.
|
|
1834
2349
|
*/
|
|
1835
2350
|
declare class LicenseError extends Error {
|
|
1836
|
-
/**
|
|
2351
|
+
/**
|
|
2352
|
+
* Resolved license status — the stable handle for branching on the cause,
|
|
2353
|
+
* since the message text is rewritten whenever it can be made clearer:
|
|
2354
|
+
*
|
|
2355
|
+
* - `none` — nothing was configured (no `licenseKey`, no gate).
|
|
2356
|
+
* - `invalid` — a key was configured but does not verify.
|
|
2357
|
+
* - `expired` / `grace` — it verifies, but the term is over.
|
|
2358
|
+
* - `revoked` — cancelled by the control plane.
|
|
2359
|
+
* - `stale` — paid and current, but unconfirmed: a connectivity problem.
|
|
2360
|
+
*
|
|
2361
|
+
* The same value is repeated at the end of the message (`license status: X`)
|
|
2362
|
+
* because an uncaught error prints nothing else.
|
|
2363
|
+
*/
|
|
1837
2364
|
readonly status?: string;
|
|
1838
2365
|
constructor(message: string, status?: string);
|
|
1839
2366
|
}
|
|
@@ -1842,10 +2369,10 @@ declare class LicenseError extends Error {
|
|
|
1842
2369
|
* Offline license verification for the paid SDK.
|
|
1843
2370
|
*
|
|
1844
2371
|
* A license is a compact signed token `base64url(header).base64url(payload).
|
|
1845
|
-
* base64url(sig)
|
|
1846
|
-
*
|
|
1847
|
-
*
|
|
1848
|
-
*
|
|
2372
|
+
* base64url(sig)`, the exact format BiVelio's licensing service mints. We verify
|
|
2373
|
+
* the Ed25519 signature against BiVelio's public key, decode the payload, and
|
|
2374
|
+
* apply the same grace policy the issuer applies. Verification is
|
|
2375
|
+
* **browser-safe**: it uses
|
|
1849
2376
|
* `@noble/curves` (pure JS) + Web primitives (`atob`, `TextEncoder`) rather than
|
|
1850
2377
|
* `node:crypto`, so the shared entry point still compiles into the browser
|
|
1851
2378
|
* demo bundle. The private signing key never leaves the control plane; only the
|
|
@@ -1943,8 +2470,8 @@ declare function deriveStatus(payload: {
|
|
|
1943
2470
|
grace_days: number;
|
|
1944
2471
|
}, nowSeconds: number, revoked?: ReadonlySet<string>): LicenseStatus;
|
|
1945
2472
|
/**
|
|
1946
|
-
* Verify + apply the grace policy → a resolved status. Mirrors
|
|
1947
|
-
*
|
|
2473
|
+
* Verify + apply the grace policy → a resolved status. Mirrors what the issuing
|
|
2474
|
+
* control plane resolves for the same token. Bad signature / malformed
|
|
1948
2475
|
* → invalid; otherwise the status comes from {@link deriveStatus}.
|
|
1949
2476
|
*/
|
|
1950
2477
|
declare function resolveLicense(token: string, publicKey: string | LicenseKeyset, opts?: {
|
|
@@ -2326,6 +2853,11 @@ interface SavingsLayerConfig {
|
|
|
2326
2853
|
compressionEconomics?: CompressionEconomics;
|
|
2327
2854
|
retriever?: Retriever;
|
|
2328
2855
|
reranker?: Reranker;
|
|
2856
|
+
/**
|
|
2857
|
+
* Tuning for the model cascade (`savings.modelCascade`). All optional; the
|
|
2858
|
+
* defaults measure and never spend.
|
|
2859
|
+
*/
|
|
2860
|
+
modelCascade?: ModelCascadeSettings;
|
|
2329
2861
|
/**
|
|
2330
2862
|
* Signed BiVelio license token (from your paid subscription). This is the
|
|
2331
2863
|
* primary way to license the SDK: `createSavingsLayer` verifies it offline
|
|
@@ -2343,10 +2875,11 @@ interface SavingsLayerConfig {
|
|
|
2343
2875
|
*/
|
|
2344
2876
|
licensePublicKey?: string | LicenseKeyset;
|
|
2345
2877
|
/**
|
|
2346
|
-
* Pre-built license gate.
|
|
2347
|
-
*
|
|
2348
|
-
*
|
|
2349
|
-
*
|
|
2878
|
+
* Pre-built license gate. **Ignored in the published npm package**, where a
|
|
2879
|
+
* {@link licenseKey} verified against the embedded BiVelio public key is the
|
|
2880
|
+
* only accepted license — passing a gate there logs a warning and changes
|
|
2881
|
+
* nothing. It exists for builds from source (this repo's own tests and
|
|
2882
|
+
* benchmarks), where it takes precedence over {@link licenseKey}.
|
|
2350
2883
|
*/
|
|
2351
2884
|
licenseGate?: LicenseGate;
|
|
2352
2885
|
/**
|
|
@@ -2469,6 +3002,41 @@ interface SavingsLayerConfig {
|
|
|
2469
3002
|
defaultCostModel?: CostModel;
|
|
2470
3003
|
/** TTL for exact-cache writes, in ms. */
|
|
2471
3004
|
exactCacheTtlMs?: number;
|
|
3005
|
+
/**
|
|
3006
|
+
* Refuse the single repair retry when paying for it would push this request's
|
|
3007
|
+
* cost above what it would have cost WITHOUT the layer. The invalid answer is
|
|
3008
|
+
* then returned as-is with `constraints.structurallyValid: false`, plus a
|
|
3009
|
+
* warning naming the three figures.
|
|
3010
|
+
*
|
|
3011
|
+
* OFF by default, and the default is a deliberate, measured choice rather than
|
|
3012
|
+
* caution — read this before turning it on.
|
|
3013
|
+
*
|
|
3014
|
+
* The bound sounds narrow ("only the worst case") and is not. The baseline of
|
|
3015
|
+
* a call that ran is reconstructed EXCLUDING the repair (see `estimateCost`:
|
|
3016
|
+
* `actualInput - repairInput`), precisely so a retry cannot pay for itself in
|
|
3017
|
+
* the report. That makes the threshold unreachable by construction: the repair
|
|
3018
|
+
* only ever moves `actual`, never the baseline, so ANY repair breaches it
|
|
3019
|
+
* whenever the layer's other books came out flat. On a request where nothing
|
|
3020
|
+
* was compressed, cached or eliminated, `baseline === actual` exactly and the
|
|
3021
|
+
* gate suppresses every repair, however cheap.
|
|
3022
|
+
*
|
|
3023
|
+
* So this is not "cap the tail", it is "stop repairing unless the layer has
|
|
3024
|
+
* already banked more than the repair will cost". That is a real quality
|
|
3025
|
+
* policy, not a rounding rule, and it is the caller's contract to decide on —
|
|
3026
|
+
* hence opt-in.
|
|
3027
|
+
*
|
|
3028
|
+
* There is no threshold that fires on the pathological case and spares the
|
|
3029
|
+
* flat one; they are the same arithmetic at different magnitudes. And there is
|
|
3030
|
+
* a genuine argument on the other side: an answer that misses its contract is
|
|
3031
|
+
* a MODEL failure, so the no-layer path would have had to pay for its own
|
|
3032
|
+
* second call too — charging the whole retry against the layer compares one
|
|
3033
|
+
* valid answer to one invalid answer.
|
|
3034
|
+
*
|
|
3035
|
+
* Turn it on when a hard per-request ceiling matters more than contract
|
|
3036
|
+
* compliance — a batch backfill, a cost-capped tenant. Leave it off when the
|
|
3037
|
+
* caller relies on the shape being right.
|
|
3038
|
+
*/
|
|
3039
|
+
skipRepairAboveBaseline?: boolean;
|
|
2472
3040
|
/**
|
|
2473
3041
|
* Reliability contract. When true (the default), ANY internal optimizer error
|
|
2474
3042
|
* (a resolver, cache, format selector, compressor, or accounting bug) is
|
|
@@ -2492,6 +3060,58 @@ interface SavingsLayerConfig {
|
|
|
2492
3060
|
/** Injectable id generator for deterministic tests. */
|
|
2493
3061
|
generateId?: () => string;
|
|
2494
3062
|
}
|
|
3063
|
+
/**
|
|
3064
|
+
* Tuning for the model cascade. Everything here is optional, and the defaults
|
|
3065
|
+
* cost nothing: with no settings at all, `modelCascade: "shadow"` measures what
|
|
3066
|
+
* it can measure for free and makes no extra provider call.
|
|
3067
|
+
*/
|
|
3068
|
+
interface ModelCascadeSettings {
|
|
3069
|
+
/**
|
|
3070
|
+
* Your own strong → cheap pairs, replacing the built-in table.
|
|
3071
|
+
*
|
|
3072
|
+
* Keys and values are model ids WITHOUT a vendor prefix (`"gpt-4o"`, not
|
|
3073
|
+
* `"openai/gpt-4o"`); the caller's prefix is carried over automatically, and a
|
|
3074
|
+
* dated snapshot inherits from its base id. A pair whose economics do not work
|
|
3075
|
+
* out at your prices is refused with a reason in the report — declaring it here
|
|
3076
|
+
* does not override the arithmetic.
|
|
3077
|
+
*/
|
|
3078
|
+
pairs?: Record<string, string>;
|
|
3079
|
+
/**
|
|
3080
|
+
* How many requests of a workload are routed before the automatic shut-off
|
|
3081
|
+
* judges the measured escalation rate (default 20).
|
|
3082
|
+
*
|
|
3083
|
+
* The rate is only observable on requests that were actually routed, so there
|
|
3084
|
+
* is no way to know without spending a bounded amount to find out. Worst case
|
|
3085
|
+
* (every one of them escalates) that exploration costs `n x r` requests'
|
|
3086
|
+
* worth of extra spend, once per workload per process — with a ratio of 0.06
|
|
3087
|
+
* and the default, about 1.2 requests.
|
|
3088
|
+
*/
|
|
3089
|
+
exploration?: number;
|
|
3090
|
+
/**
|
|
3091
|
+
* PRICED OPT-IN, default 0 (off). Fraction of `"shadow"` requests on which the
|
|
3092
|
+
* layer also calls the cheap model, purely to measure.
|
|
3093
|
+
*
|
|
3094
|
+
* Shadow mode is free precisely because it does not run the cheap model: it
|
|
3095
|
+
* reports eligibility, the arithmetic, and what the swap would have been worth.
|
|
3096
|
+
* What it cannot know for free is the one number that decides everything — how
|
|
3097
|
+
* often the deterministic gate would have REJECTED the cheap answer. That
|
|
3098
|
+
* answer does not exist until the cheap model writes one.
|
|
3099
|
+
*
|
|
3100
|
+
* So this buys it, and the price is exact: each sampled request costs one extra
|
|
3101
|
+
* cheap call, i.e. `r` of what the request already cost — about 6% for
|
|
3102
|
+
* gpt-4o → gpt-4o-mini. At `0.05` that is roughly 0.3% on top of your bill, and
|
|
3103
|
+
* every sampled request reports its own `modelCascade.sampleCost`. It also
|
|
3104
|
+
* DOUBLES that request's provider latency, because the extra call is sequential.
|
|
3105
|
+
*
|
|
3106
|
+
* The sampled request's tokens count as spend and are held out of the baseline,
|
|
3107
|
+
* exactly like the repair retry: measurement must never pay for itself.
|
|
3108
|
+
*
|
|
3109
|
+
* Which requests get sampled is derived from the request's own cache key, so it
|
|
3110
|
+
* is deterministic and stable — the same request is always sampled or always
|
|
3111
|
+
* not, and no `Math.random` decides how your money is spent.
|
|
3112
|
+
*/
|
|
3113
|
+
shadowSampleRate?: number;
|
|
3114
|
+
}
|
|
2495
3115
|
declare const DEFAULT_COST_MODEL: CostModel;
|
|
2496
3116
|
|
|
2497
3117
|
/** Fully-resolved, non-optional dependency set the pipeline runs against. */
|
|
@@ -2531,9 +3151,15 @@ interface SavingsLayer {
|
|
|
2531
3151
|
* cache, a vector-backed semantic cache, a real tokenizer, a compressor sidecar,
|
|
2532
3152
|
* XGrammar/llguidance backends, etc.).
|
|
2533
3153
|
*
|
|
3154
|
+
* A `licenseKey` is not optional in practice: `generate()` is fail-closed and
|
|
3155
|
+
* throws {@link LicenseError} without one, so it belongs in the smallest example
|
|
3156
|
+
* there is.
|
|
3157
|
+
*
|
|
2534
3158
|
* @example
|
|
2535
|
-
*
|
|
2536
|
-
*
|
|
3159
|
+
* // `licenseKey` is the signed token from savings.bivelio.com/dashboard/install,
|
|
3160
|
+
* // read from wherever you keep secrets (BIVELIO_LICENSE_KEY in the README).
|
|
3161
|
+
* const layer = createSavingsLayer({ provider, licenseKey });
|
|
3162
|
+
* const { text, report } = await layer.generate({
|
|
2537
3163
|
* model: "openai/gpt-4o-mini",
|
|
2538
3164
|
* messages: [{ role: "user", content: "What is 2 + 2 * 3?" }],
|
|
2539
3165
|
* });
|
|
@@ -2567,6 +3193,6 @@ declare function createSavingsLayer(config: Omit<SavingsLayerConfig, "tokenizer"
|
|
|
2567
3193
|
* constant to package.json so the two can never drift.
|
|
2568
3194
|
*/
|
|
2569
3195
|
|
|
2570
|
-
declare const BV_SALA_VERSION = "0.
|
|
3196
|
+
declare const BV_SALA_VERSION = "0.4.0";
|
|
2571
3197
|
|
|
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 };
|
|
3198
|
+
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, UMBRAL_LOCAL, UsageEmitter, type UsageEmitterOptions, type ValidationError, type ValidationResult, adaptiveK, arithmeticResolver, buildExactKey, buildInflightKey, canonicalize, compactRowsSerializer, computeNextDelay, cosine, createCompactRowsSerializer, createDefaultResolvers, createHandoff, createMockProvider, createSavingsLayer, csvSerializer, decodeCompactRows, dedupe, defaultTokenizerRegistry, deriveSeatId, deriveStatus, detectCi, embebedorLocal, emptyState, encodeCompactRows, evalArithmetic, evaluatePolicy, executeRetrievalPlan, gateFromKey, heuristicEstimator, isKeyset, isStrictCompatible, jsonCompactSerializer, keyTokensMatch, liveGateFromKey, mergeDelta, modelFamilyOf, noopCompressor, normalizeText, precioDeLista, projectContext, providerNativeBackend, randomId, registerExactTokenizer, registrarTokenizadorExacto, renderState, resolveConstraintBackend, resolveDependencies, resolveLicense, sha256, shouldCompress, stableStringify, uniformColumns, validateAgainstSchema, vectorLocal, verifyCompactSignature, verifyLicenseToken, verifyRevocationList, withProtectedSpans };
|