@bivelio/savings-layer 0.2.2 → 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/README.md +36 -38
- package/dist/index.d.ts +966 -76
- package/dist/index.js +23 -21
- package/package.json +7 -11
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Environment-agnostic UUID generation.
|
|
3
|
-
* ---------------------------------------------------------------------------
|
|
4
|
-
* Trace IDs only need to be unique, not cryptographically unpredictable. We
|
|
5
|
-
* prefer the platform `crypto.randomUUID` (present in modern Node, browsers,
|
|
6
|
-
* Deno and edge runtimes) and fall back to a v4-shaped id when it is absent, so
|
|
7
|
-
* BV-SALA runs everywhere without a `node:crypto` import that breaks bundlers.
|
|
8
|
-
*/
|
|
9
|
-
declare function randomId(): string;
|
|
10
|
-
|
|
11
1
|
/**
|
|
12
2
|
* The four-ledger savings accounting.
|
|
13
3
|
*
|
|
@@ -75,6 +65,39 @@ declare class SavingsLedger {
|
|
|
75
65
|
* cost; work the layer does to fix its own output has no place in it.
|
|
76
66
|
*/
|
|
77
67
|
repairInput: number;
|
|
68
|
+
/**
|
|
69
|
+
* INPUT of the schema-on-demand discovery probe. Held apart exactly like
|
|
70
|
+
* {@link repairInput}, and for the same reason.
|
|
71
|
+
*
|
|
72
|
+
* The layer ADDS this call; the naive path would never make it. Letting its
|
|
73
|
+
* tokens into the reconstructed baseline would make the probe pay for itself,
|
|
74
|
+
* and a request that cost MORE than going without the layer would publish a
|
|
75
|
+
* positive saving. It still counts in full towards `actualInput` — that is
|
|
76
|
+
* money genuinely spent — it just cannot inflate what that is compared against.
|
|
77
|
+
*/
|
|
78
|
+
discoveryInput: number;
|
|
79
|
+
/** OUTPUT of the discovery probe. Our cost, kept out of the baseline. */
|
|
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;
|
|
78
101
|
/**
|
|
79
102
|
* Lo que de verdad hizo la compresión, para publicarlo en
|
|
80
103
|
* `SavingsReport.compression`. El campo estaba declarado en el contrato y no lo
|
|
@@ -113,6 +136,53 @@ declare class SavingsLedger {
|
|
|
113
136
|
};
|
|
114
137
|
}
|
|
115
138
|
|
|
139
|
+
/**
|
|
140
|
+
* LIST PRICES PER MODEL.
|
|
141
|
+
*
|
|
142
|
+
* The layer reports savings in money, and money needs a price. This is the
|
|
143
|
+
* built-in catalogue of published list prices, so the dollar figures mean
|
|
144
|
+
* something without any configuration.
|
|
145
|
+
*
|
|
146
|
+
* ⚠️ A WRONG PRICE HERE IS WORSE THAN NO CATALOGUE. A model that is ABSENT falls
|
|
147
|
+
* back to the illustrative default AND warns, so the customer knows that column
|
|
148
|
+
* is not their invoice. A model that is PRESENT with the wrong price silences
|
|
149
|
+
* that warning and hands over a confident, wrong number. So only models whose
|
|
150
|
+
* published price is known with certainty belong here, and {@link PRECIOS_AL_DIA}
|
|
151
|
+
* records when they were last checked.
|
|
152
|
+
*
|
|
153
|
+
* Prices are USD per 1,000 tokens — the unit of `CostModel` — even though
|
|
154
|
+
* providers publish per million: 1M to 1K is a division by a thousand.
|
|
155
|
+
*
|
|
156
|
+
* `costModels` on the layer config ALWAYS wins over this. A customer with volume
|
|
157
|
+
* discounts, negotiated rates or a different region sets their own and this
|
|
158
|
+
* catalogue stays out of the way.
|
|
159
|
+
*/
|
|
160
|
+
/** When these prices were last checked against the providers' public rates. */
|
|
161
|
+
declare const PRECIOS_AL_DIA = "2026-08-13";
|
|
162
|
+
/**
|
|
163
|
+
* Key = the model id as the provider publishes it, without a vendor prefix.
|
|
164
|
+
* Lookup tries the exact id, then the id without its provider prefix, then the
|
|
165
|
+
* LONGEST matching prefix — so `gpt-4o-2024-08-06` inherits `gpt-4o` without
|
|
166
|
+
* having to enumerate every dated snapshot.
|
|
167
|
+
*/
|
|
168
|
+
declare const CATALOGO: Readonly<Record<string, CostModel>>;
|
|
169
|
+
/**
|
|
170
|
+
* The list price for a model, or `undefined` when it is not in the catalogue.
|
|
171
|
+
*
|
|
172
|
+
* @param model the id as the caller passes it, with or without a vendor prefix.
|
|
173
|
+
*/
|
|
174
|
+
declare function precioDeLista(model: string): CostModel | undefined;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Environment-agnostic UUID generation.
|
|
178
|
+
* ---------------------------------------------------------------------------
|
|
179
|
+
* Trace IDs only need to be unique, not cryptographically unpredictable. We
|
|
180
|
+
* prefer the platform `crypto.randomUUID` (present in modern Node, browsers,
|
|
181
|
+
* Deno and edge runtimes) and fall back to a v4-shaped id when it is absent, so
|
|
182
|
+
* BV-SALA runs everywhere without a `node:crypto` import that breaks bundlers.
|
|
183
|
+
*/
|
|
184
|
+
declare function randomId(): string;
|
|
185
|
+
|
|
116
186
|
/**
|
|
117
187
|
* Core contracts for BV-SALA (bivelio-savings-layer).
|
|
118
188
|
*
|
|
@@ -126,8 +196,8 @@ declare class SavingsLedger {
|
|
|
126
196
|
* - provider_cached_tokens tokens still in the prompt that the provider may
|
|
127
197
|
* bill or process differently (prefix/KV caching).
|
|
128
198
|
*
|
|
129
|
-
* A cache hit eliminates a whole call.
|
|
130
|
-
*
|
|
199
|
+
* A cache hit eliminates a whole call. A compact encoding re-encodes a structured payload.
|
|
200
|
+
* A context compressor removes redundant words. A provider prefix cache makes tokens
|
|
131
201
|
* cheaper without removing them. Mixing these produces misleading metrics.
|
|
132
202
|
*/
|
|
133
203
|
/** Roles understood by the provider-neutral chat interface. */
|
|
@@ -187,7 +257,7 @@ interface OutputContract {
|
|
|
187
257
|
version?: string;
|
|
188
258
|
}
|
|
189
259
|
/** Serialization formats the format-selector can choose between. */
|
|
190
|
-
type SerializationFormat = "json-compact" | "
|
|
260
|
+
type SerializationFormat = "json-compact" | "compact-rows" | "csv";
|
|
191
261
|
interface RetrievalOptions {
|
|
192
262
|
mode: "off" | "auto" | "single" | "multi";
|
|
193
263
|
maxRounds?: number;
|
|
@@ -201,6 +271,338 @@ interface SavingsToggles {
|
|
|
201
271
|
singleflight?: boolean;
|
|
202
272
|
deterministicResolvers?: boolean;
|
|
203
273
|
router?: boolean;
|
|
274
|
+
/**
|
|
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.
|
|
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.
|
|
300
|
+
*/
|
|
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;
|
|
324
|
+
/**
|
|
325
|
+
* OPT-IN, and it changes the latency contract: where it applies, one request
|
|
326
|
+
* becomes TWO provider calls, one after the other.
|
|
327
|
+
*
|
|
328
|
+
* With a large tool catalogue the full JSON schema of every candidate travels
|
|
329
|
+
* on every request, even though the model will use one tool or none. With this
|
|
330
|
+
* on, the layer may first send lightweight descriptors (name + one line, no
|
|
331
|
+
* `parameters`), read which tools the model asks for, and only then send those
|
|
332
|
+
* full schemas.
|
|
333
|
+
*
|
|
334
|
+
* It is applied only when the token arithmetic for THIS request, priced with
|
|
335
|
+
* your cost model, says the second call pays for itself with margin — so a
|
|
336
|
+
* small catalogue silently keeps the single-call path.
|
|
337
|
+
*
|
|
338
|
+
* THE MARGIN IS NOT A GUARANTEE THAT EVERY REQUEST GETS CHEAPER. When the
|
|
339
|
+
* model names no tool in the first call, the request still goes out with every
|
|
340
|
+
* schema and the first call is pure added cost: that request costs MORE, and
|
|
341
|
+
* the report says so (`cost.netSavings` goes negative). The margin required is
|
|
342
|
+
* exactly the cost of that outcome, so the pattern breaks even while it
|
|
343
|
+
* happens on at most one request in two. If your traffic asks for a tool less
|
|
344
|
+
* often than that — a chat assistant with a big catalogue answers most turns
|
|
345
|
+
* directly — leave this off. Only an A/B against your own traffic settles it.
|
|
346
|
+
*
|
|
347
|
+
* IT ALSO GIVES UP THE PROVIDER'S PREFIX CACHE, on every request, not just the
|
|
348
|
+
* ones where it applies. Tools render FIRST in a provider request, and with
|
|
349
|
+
* this on, which schemas travel is decided by a probe whose answer is not
|
|
350
|
+
* deterministic — so the front of the prompt varies between requests that
|
|
351
|
+
* would otherwise share a cacheable prefix. Marking one would pay the write
|
|
352
|
+
* premium (1.25x on Anthropic) for a prefix that cannot hit. If your prompts
|
|
353
|
+
* carry a long fixed system header and your traffic repeats it, that discount
|
|
354
|
+
* is very likely worth more than this lever: measure both before choosing.
|
|
355
|
+
* `report.traces` says which one is off and why.
|
|
356
|
+
*
|
|
357
|
+
* When the second call happened, `report.calls.actualLlm` is 2. The extra
|
|
358
|
+
* call's tokens are counted as spend and never credited to your baseline.
|
|
359
|
+
* Write and destructive tools always travel with their full schema, so no
|
|
360
|
+
* action can be lost to a discovery step.
|
|
361
|
+
*/
|
|
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;
|
|
204
606
|
}
|
|
205
607
|
interface RequestContext {
|
|
206
608
|
tenantId?: string;
|
|
@@ -266,8 +668,61 @@ interface SavingsResult<T = unknown> {
|
|
|
266
668
|
report: SavingsReport;
|
|
267
669
|
}
|
|
268
670
|
type CacheOutcome = "miss" | "exact" | "semantic" | "singleflight" | "deterministic";
|
|
671
|
+
/**
|
|
672
|
+
* The quality claims this layer is able to DEMONSTRATE for the answer it served.
|
|
673
|
+
*
|
|
674
|
+
* A boolean cannot carry this. `constraints.structurallyValid` puts an answer
|
|
675
|
+
* computed by a deterministic resolver — exact by construction — in the same box
|
|
676
|
+
* as one the model wrote while nobody checked anything but its shape. Those two
|
|
677
|
+
* are the difference between "this is the result" and "this is what the model
|
|
678
|
+
* said", and a product that claims savings owes the reader that distinction.
|
|
679
|
+
*
|
|
680
|
+
* THE RULE BEHIND THIS VOCABULARY: never report "no loss" on the strength of
|
|
681
|
+
* cosine similarity, BLEU, ROUGE or another model's judgement. Those scores stay
|
|
682
|
+
* high while a figure, a negation or an exception quietly disappears — precisely
|
|
683
|
+
* the fields that decide a legal or financial document. A claim of equivalence
|
|
684
|
+
* rests on a deterministic gate, or it is not made.
|
|
685
|
+
*
|
|
686
|
+
* - `exact` exact by construction: a deterministic resolver computed
|
|
687
|
+
* the answer, or an exact-cache hit replayed the answer
|
|
688
|
+
* already produced for this very same request.
|
|
689
|
+
* - `task_equivalent` not byte-identical, but it cleared a deterministic gate
|
|
690
|
+
* before being served — numbers and dates as a multiset,
|
|
691
|
+
* negation parity and a key-entity floor all had to agree,
|
|
692
|
+
* on top of any similarity score.
|
|
693
|
+
* - `not_verified` the model was called and nothing beyond the shape of the
|
|
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}.
|
|
703
|
+
* - `rejected` what was served does not satisfy its output contract, and
|
|
704
|
+
* a repair either failed or would have been a wasted call.
|
|
705
|
+
* The answer is still returned: this is the layer's verdict
|
|
706
|
+
* on it, not a refusal to hand it over.
|
|
707
|
+
*
|
|
708
|
+
* The list is deliberately short. A status nobody produces is a promise the
|
|
709
|
+
* reader cannot check, so levers that do not exist yet bring their own status
|
|
710
|
+
* the day they can demonstrate it. Read this as an OPEN set — keep a default
|
|
711
|
+
* branch, exactly as with a licence status.
|
|
712
|
+
*/
|
|
713
|
+
declare const QUALITY_STATUSES: readonly ["exact", "task_equivalent", "not_verified", "bounded_degradation", "rejected"];
|
|
714
|
+
type QualityStatus = (typeof QUALITY_STATUSES)[number];
|
|
269
715
|
interface SavingsReport {
|
|
270
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
|
+
*/
|
|
271
726
|
model: string;
|
|
272
727
|
calls: {
|
|
273
728
|
baselineLlm: number;
|
|
@@ -328,8 +783,38 @@ interface SavingsReport {
|
|
|
328
783
|
*/
|
|
329
784
|
structurallyValid?: boolean;
|
|
330
785
|
};
|
|
786
|
+
/**
|
|
787
|
+
* How the served answer earned its place, in one word. Optional and additive:
|
|
788
|
+
* a report built by an older SDK simply does not carry it, so treat an absent
|
|
789
|
+
* value as "unknown", never as a pass.
|
|
790
|
+
*
|
|
791
|
+
* It only ever DEGRADES what the rest of the report claims. An answer that
|
|
792
|
+
* fails its output contract is `rejected` however it was produced — a resolver
|
|
793
|
+
* that computes the right number for the wrong contract does not get to keep
|
|
794
|
+
* the `exact` label.
|
|
795
|
+
*/
|
|
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;
|
|
331
808
|
quality: {
|
|
332
809
|
taskPassed?: boolean;
|
|
810
|
+
/**
|
|
811
|
+
* Similarity score of the retained answer, when one was computed.
|
|
812
|
+
*
|
|
813
|
+
* A number here is NOT evidence of equivalence: see {@link QualityStatus}.
|
|
814
|
+
* Cosine (and BLEU, ROUGE, or an LLM judge) can sit above 0.95 across a
|
|
815
|
+
* changed figure or a dropped negation, so it informs a decision — it never
|
|
816
|
+
* licenses a "no loss" claim.
|
|
817
|
+
*/
|
|
333
818
|
semanticScore?: number;
|
|
334
819
|
freshnessPassed?: boolean;
|
|
335
820
|
};
|
|
@@ -364,10 +849,14 @@ interface SavingsReport {
|
|
|
364
849
|
* Structural license gate the pipeline consults before enabling a managed
|
|
365
850
|
* feature (semantic cache, learned thresholds, long-context compression, …).
|
|
366
851
|
*
|
|
367
|
-
* It is intentionally a bare shape
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
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.
|
|
371
860
|
*/
|
|
372
861
|
interface LicenseGate {
|
|
373
862
|
isEntitled(feature: string): boolean;
|
|
@@ -410,7 +899,7 @@ interface TraceEvent {
|
|
|
410
899
|
* be re-explained in free-form natural language at every hop. BV-SALA replaces
|
|
411
900
|
* that with a compact, typed handoff object. Each consumer receives a
|
|
412
901
|
* projection of the state, not the whole conversation. The serializer then
|
|
413
|
-
* decides whether to send it as compact JSON or
|
|
902
|
+
* decides whether to send it as compact JSON or compact rows.
|
|
414
903
|
*/
|
|
415
904
|
interface HandoffFact {
|
|
416
905
|
id: string;
|
|
@@ -460,7 +949,7 @@ interface TokenEstimator {
|
|
|
460
949
|
* El heurístico vale para RANKEAR formatos candidatos (le basta con acertar el
|
|
461
950
|
* orden). No vale para restar tokens entre formatos distintos: cobra cada
|
|
462
951
|
* símbolo suelto como un token entero, así que infla el JSON denso en símbolos
|
|
463
|
-
* mucho más que el
|
|
952
|
+
* mucho más que el the compact encoder, y la diferencia se acredita como ahorro inexistente.
|
|
464
953
|
*/
|
|
465
954
|
readonly exact: boolean;
|
|
466
955
|
/** Estimate the number of tokens in a string. */
|
|
@@ -517,18 +1006,9 @@ declare const defaultTokenizerRegistry: TokenizerRegistry;
|
|
|
517
1006
|
* contamos aquí. Con el estimador heurístico esos dos sumandos vienen de reglas
|
|
518
1007
|
* distintas, y la diferencia NO es ruido: el heurístico cobra cada símbolo suelto
|
|
519
1008
|
* como un token entero, así que infla el JSON (40% de símbolos) mucho más que el
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
* Cómo se instala. `gpt-tokenizer` son ~53 MB desempaquetados, así que NO es
|
|
524
|
-
* dependencia dura: imponérsela a todo el que instale el SDK, cuando la mayoría
|
|
525
|
-
* no necesita contabilidad medida, sería abusivo. Es peerDependency opcional:
|
|
526
|
-
*
|
|
527
|
-
* pnpm add gpt-tokenizer
|
|
528
|
-
*
|
|
529
|
-
* Sin ella el layer sigue funcionando con el heurístico, y el `basis` de cada
|
|
530
|
-
* informe lo dice — "estimated" en vez de "measured". Esa es la diferencia
|
|
531
|
-
* importante: el producto no deja de funcionar, deja de AFIRMAR de más.
|
|
1009
|
+
* An estimator that is not calibrated for the destination model can miss by
|
|
1010
|
+
* enough to publish a baseline several times the real one, which would credit a
|
|
1011
|
+
* saving that never existed. That is why `basis` degrades instead of claiming.
|
|
532
1012
|
*/
|
|
533
1013
|
|
|
534
1014
|
/** Lo que necesitamos de gpt-tokenizer. Tipado aquí para no depender de sus tipos. */
|
|
@@ -588,7 +1068,11 @@ interface CanonicalRequest {
|
|
|
588
1068
|
model: string;
|
|
589
1069
|
modelFamily: string;
|
|
590
1070
|
messages: ChatMessage[];
|
|
591
|
-
/**
|
|
1071
|
+
/**
|
|
1072
|
+
* Text of the user-visible turns, normalized so that trivial near-duplicates
|
|
1073
|
+
* collide: Unicode form, case, whitespace, typographic marks and sentence
|
|
1074
|
+
* punctuation. Numbers, dates, quantities and negations are preserved exactly.
|
|
1075
|
+
*/
|
|
592
1076
|
normalizedText: string;
|
|
593
1077
|
systemPromptHash: string;
|
|
594
1078
|
toolsetHash: string;
|
|
@@ -616,10 +1100,26 @@ interface CanonicalRequest {
|
|
|
616
1100
|
tokenizer: string | undefined;
|
|
617
1101
|
original: SavingsRequest;
|
|
618
1102
|
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Normalized form of a turn's text, as it enters the cache key.
|
|
1105
|
+
*
|
|
1106
|
+
* Folded away: Unicode form, case, whitespace, typographic marks, Latin
|
|
1107
|
+
* diacritics and sentence punctuation. Preserved exactly: numbers, decimal and
|
|
1108
|
+
* thousands separators, times, dates, units, negations, math operators and any
|
|
1109
|
+
* combining mark outside the Latin alphabet.
|
|
1110
|
+
*/
|
|
619
1111
|
declare function normalizeText(text: string): string;
|
|
620
1112
|
declare function sha256(input: string): string;
|
|
621
1113
|
/** Stable JSON stringify with sorted keys so hashes are order-independent. */
|
|
622
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;
|
|
623
1123
|
declare function canonicalize(request: SavingsRequest): CanonicalRequest;
|
|
624
1124
|
|
|
625
1125
|
/**
|
|
@@ -629,8 +1129,21 @@ declare function canonicalize(request: SavingsRequest): CanonicalRequest;
|
|
|
629
1129
|
* text but differ in tenant, permission scope, model version, system prompt,
|
|
630
1130
|
* schema, toolset, locale, sampling, knowledge epoch or safety policy — any of
|
|
631
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.
|
|
632
1143
|
*/
|
|
633
|
-
declare function buildExactKey(c: CanonicalRequest
|
|
1144
|
+
declare function buildExactKey(c: CanonicalRequest, opciones?: {
|
|
1145
|
+
servedModel?: string;
|
|
1146
|
+
}): string;
|
|
634
1147
|
/**
|
|
635
1148
|
* In-flight (singleflight) key. Coalesces concurrent, equivalent requests onto
|
|
636
1149
|
* a single execution. It can be slightly looser than the exact key (it does not
|
|
@@ -641,8 +1154,37 @@ declare function buildExactKey(c: CanonicalRequest): string;
|
|
|
641
1154
|
* two concurrent calls that differ only in `schemaVersion` are different
|
|
642
1155
|
* requests and must never coalesce. All three doors — exact key, in-flight key
|
|
643
1156
|
* and semantic filter — key on the same identity, by design.
|
|
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.
|
|
644
1183
|
*/
|
|
645
|
-
declare function buildInflightKey(c: CanonicalRequest
|
|
1184
|
+
declare function buildInflightKey(c: CanonicalRequest, opciones?: {
|
|
1185
|
+
servedModel?: string;
|
|
1186
|
+
deferrable?: boolean;
|
|
1187
|
+
}): string;
|
|
646
1188
|
|
|
647
1189
|
/**
|
|
648
1190
|
* Policy engine: risk and freshness.
|
|
@@ -821,7 +1363,7 @@ declare function cosine(a: number[], b: number[]): number;
|
|
|
821
1363
|
declare function keyTokensMatch(a: string, b: string): boolean;
|
|
822
1364
|
/**
|
|
823
1365
|
* In-memory reference implementation. A production build would back this with a
|
|
824
|
-
* real vector store
|
|
1366
|
+
* real vector store and metadata filters pushed down
|
|
825
1367
|
* to the store. The verification logic — the part that makes it *safe* — stays
|
|
826
1368
|
* identical regardless of the backend.
|
|
827
1369
|
*/
|
|
@@ -936,6 +1478,17 @@ interface ToolSelection {
|
|
|
936
1478
|
tools: ToolDescriptor[];
|
|
937
1479
|
droppedIds: string[];
|
|
938
1480
|
reason: string;
|
|
1481
|
+
/**
|
|
1482
|
+
* Whether the SET of tools is determined by something stable across requests,
|
|
1483
|
+
* rather than by this request's text.
|
|
1484
|
+
*
|
|
1485
|
+
* It is not cosmetic. Tools render FIRST in a provider request (tools ->
|
|
1486
|
+
* system -> messages), so a tool set that changes with the question moves the
|
|
1487
|
+
* very front of the prompt and invalidates the whole prefix cache behind it.
|
|
1488
|
+
* A caller whose prompt caching silently never hits, on traffic with tools, is
|
|
1489
|
+
* usually looking at this.
|
|
1490
|
+
*/
|
|
1491
|
+
setIsStable: boolean;
|
|
939
1492
|
}
|
|
940
1493
|
interface ToolSelectorOptions {
|
|
941
1494
|
/** Max tools to include (schema-token budget guard). */
|
|
@@ -1072,7 +1625,7 @@ interface Serializer {
|
|
|
1072
1625
|
encode(value: unknown): string;
|
|
1073
1626
|
/**
|
|
1074
1627
|
* The instruction text a model needs to interpret this format. For JSON this
|
|
1075
|
-
* is empty; for
|
|
1628
|
+
* is empty; for row/CSV it is the per-request cost that must be amortized
|
|
1076
1629
|
* before the format is worthwhile.
|
|
1077
1630
|
*/
|
|
1078
1631
|
instruction(value: unknown): string;
|
|
@@ -1090,60 +1643,60 @@ interface Serializer {
|
|
|
1090
1643
|
* Compact JSON: the lossless baseline every other format is measured against.
|
|
1091
1644
|
* No indentation, no incidental whitespace. This is frequently the *winner* for
|
|
1092
1645
|
* deeply nested or non-uniform data, which is exactly why it is the baseline and
|
|
1093
|
-
*
|
|
1646
|
+
* The compact encoder is only ever a challenger.
|
|
1094
1647
|
*/
|
|
1095
1648
|
declare const jsonCompactSerializer: Serializer;
|
|
1096
1649
|
|
|
1097
1650
|
/**
|
|
1098
|
-
*
|
|
1651
|
+
* Compact row encoder.
|
|
1099
1652
|
*
|
|
1100
|
-
*
|
|
1653
|
+
* Its strength is UNIFORM ARRAYS OF FLAT OBJECTS: it declares the column
|
|
1101
1654
|
* names once and then emits one row per object, which removes the repeated keys
|
|
1102
1655
|
* that make JSON expensive for tabular data. On deeply nested or non-uniform
|
|
1103
|
-
* data
|
|
1656
|
+
* data it can be *larger* than compact JSON — so this is only ever a candidate
|
|
1104
1657
|
* the selector may pick, never a default.
|
|
1105
1658
|
*
|
|
1106
1659
|
* This is an in-house, pinned encoder placed behind the {@link Serializer}
|
|
1107
1660
|
* interface (as recommended: do not couple the system to an upstream `main`).
|
|
1108
|
-
* It emits a well-defined subset and ships a matching decoder so the JSON<->
|
|
1661
|
+
* It emits a well-defined subset and ships a matching decoder so the JSON<->rows
|
|
1109
1662
|
* round-trip is deterministic and testable.
|
|
1110
1663
|
*/
|
|
1111
|
-
interface
|
|
1664
|
+
interface CompactRowsOptions {
|
|
1112
1665
|
/** Field delimiter within tabular/inline rows. */
|
|
1113
1666
|
delimiter?: "," | "\t";
|
|
1114
1667
|
/** Indentation unit. */
|
|
1115
1668
|
indent?: string;
|
|
1116
1669
|
}
|
|
1117
|
-
declare function
|
|
1670
|
+
declare function encodeCompactRows(value: unknown, options?: CompactRowsOptions): string;
|
|
1118
1671
|
/** Return shared column names iff every element is a flat, scalar-valued object with identical keys. */
|
|
1119
1672
|
declare function uniformColumns(arr: unknown[]): string[] | null;
|
|
1120
1673
|
/**
|
|
1121
|
-
* The instruction a model needs to read
|
|
1122
|
-
* is the per-request overhead the selector must amortize before
|
|
1674
|
+
* The instruction a model needs to read the compact encoder. Kept short on purpose — this text
|
|
1675
|
+
* is the per-request overhead the selector must amortize before it wins.
|
|
1123
1676
|
*/
|
|
1124
|
-
declare const
|
|
1125
|
-
declare function
|
|
1126
|
-
declare const
|
|
1677
|
+
declare const COMPACT_ROWS_INSTRUCTION: string;
|
|
1678
|
+
declare function createCompactRowsSerializer(options?: CompactRowsOptions): Serializer;
|
|
1679
|
+
declare const compactRowsSerializer: Serializer;
|
|
1127
1680
|
|
|
1128
1681
|
/**
|
|
1129
|
-
*
|
|
1682
|
+
* Decoder for the subset emitted by {@link encodeCompactRows}.
|
|
1130
1683
|
*
|
|
1131
1684
|
* Supports: scalar fields, nested objects, empty arrays, inline scalar arrays
|
|
1132
1685
|
* (`key[N]: a,b,c`) and tabular object arrays (`key[N]{c1,c2}:` + rows). This
|
|
1133
|
-
* covers the cases the format selector actually chooses
|
|
1134
|
-
* JSON<->
|
|
1686
|
+
* covers the cases the format selector actually chooses it for, and makes the
|
|
1687
|
+
* JSON<->rows round-trip deterministic and testable. List-form arrays of nested
|
|
1135
1688
|
* objects are intentionally out of scope for decoding (the selector never picks
|
|
1136
|
-
*
|
|
1689
|
+
* it for those, since JSON wins there anyway).
|
|
1137
1690
|
*/
|
|
1138
|
-
interface
|
|
1691
|
+
interface CompactRowsDecodeOptions {
|
|
1139
1692
|
delimiter?: "," | "\t";
|
|
1140
1693
|
indent?: string;
|
|
1141
1694
|
}
|
|
1142
|
-
declare function
|
|
1695
|
+
declare function decodeCompactRows(text: string, options?: CompactRowsDecodeOptions): unknown;
|
|
1143
1696
|
|
|
1144
1697
|
/**
|
|
1145
1698
|
* CSV serializer — only valid for genuinely tabular data (a uniform array of
|
|
1146
|
-
* flat objects). CSV can be marginally smaller than
|
|
1699
|
+
* flat objects). CSV can be marginally smaller than the compact rows on pure tables, but it
|
|
1147
1700
|
* does NOT preserve the JSON data model in general, so `supports()` is strict.
|
|
1148
1701
|
*/
|
|
1149
1702
|
declare const csvSerializer: Serializer;
|
|
@@ -1151,7 +1704,7 @@ declare const csvSerializer: Serializer;
|
|
|
1151
1704
|
/**
|
|
1152
1705
|
* Format selector.
|
|
1153
1706
|
*
|
|
1154
|
-
*
|
|
1707
|
+
* It is a candidate, never a default. For each request we count, USING THE
|
|
1155
1708
|
* DESTINATION MODEL'S TOKENIZER:
|
|
1156
1709
|
*
|
|
1157
1710
|
* tokens(encoded payload) + tokens(instruction to understand it)
|
|
@@ -1170,7 +1723,7 @@ interface FormatSelectorOptions {
|
|
|
1170
1723
|
minimumSavingsRatio?: number;
|
|
1171
1724
|
/**
|
|
1172
1725
|
* Per-(model,format) quality penalty in tokens. Encodes "this model is worse
|
|
1173
|
-
* at reading
|
|
1726
|
+
* at reading the compact rows" as an additive cost, learned from production traces.
|
|
1174
1727
|
*/
|
|
1175
1728
|
qualityPenalty?: (model: string, format: SerializationFormat) => number;
|
|
1176
1729
|
}
|
|
@@ -1274,7 +1827,7 @@ declare function resolveConstraintBackend(model: string, schema: JsonSchema, bac
|
|
|
1274
1827
|
declare function providerNativeBackend(isSupported?: (model: string, schema: JsonSchema) => boolean): ConstraintBackend;
|
|
1275
1828
|
|
|
1276
1829
|
/**
|
|
1277
|
-
* Context compression
|
|
1830
|
+
* Context compression, as an OPTIONAL sidecar.
|
|
1278
1831
|
*
|
|
1279
1832
|
* Two rules make this safe and worthwhile:
|
|
1280
1833
|
*
|
|
@@ -1343,9 +1896,60 @@ declare function shouldCompress(input: CompressionPolicyInput): {
|
|
|
1343
1896
|
* worse than no compressor at all.
|
|
1344
1897
|
*/
|
|
1345
1898
|
declare function withProtectedSpans(inner: Compressor): Compressor;
|
|
1346
|
-
/** No-op compressor: the safe default until a real
|
|
1899
|
+
/** No-op compressor: the safe default until a real compressor sidecar is wired in. */
|
|
1347
1900
|
declare const noopCompressor: Compressor;
|
|
1348
1901
|
|
|
1902
|
+
/** What the layer needs to know about a provider's prefix cache to price a decision. */
|
|
1903
|
+
interface CacheTier {
|
|
1904
|
+
/**
|
|
1905
|
+
* Minimum prompt tokens before the provider caches the prefix at all;
|
|
1906
|
+
* `null` means it never does.
|
|
1907
|
+
*
|
|
1908
|
+
* The built-in tiers assume 1024 for every model. That is an ASSUMPTION, not a
|
|
1909
|
+
* verified figure: real minimums differ per model and family. Set your model's
|
|
1910
|
+
* own minimum here when you know it.
|
|
1911
|
+
*/
|
|
1912
|
+
minimumCacheTokens: number | null;
|
|
1913
|
+
/** Price of a cache-read token as a fraction of a normal one (e.g. 0.5, 0.1). */
|
|
1914
|
+
cacheReadMultiplier: number;
|
|
1915
|
+
/** Surcharge for WRITING the cache the first time; some providers bill above 1. */
|
|
1916
|
+
cacheWriteMultiplier: number;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
/** Tunes when compression is worth its money. Every field is optional. */
|
|
1920
|
+
interface CompressionEconomics {
|
|
1921
|
+
/**
|
|
1922
|
+
* The provider's prefix-cache tier. Derived from the request's model when
|
|
1923
|
+
* omitted; set it to state your model's real cache minimum and multipliers.
|
|
1924
|
+
*/
|
|
1925
|
+
cacheTier?: CacheTier;
|
|
1926
|
+
/**
|
|
1927
|
+
* YOUR MEASURED prefix-cache economics: how many prompt tokens are a stable
|
|
1928
|
+
* prefix across requests, and how often that prefix arrives already warm
|
|
1929
|
+
* (`report.tokens.providerCachedInput` is the numerator for the rate).
|
|
1930
|
+
*
|
|
1931
|
+
* Both are required together — a hit rate over an unknown prefix says nothing
|
|
1932
|
+
* about how much money is at stake. Declaring them unlocks the full decision,
|
|
1933
|
+
* including knowingly trading the prefix cache for a smaller prompt when that
|
|
1934
|
+
* is the cheaper side. Left out, the layer refuses to compress a prompt out of
|
|
1935
|
+
* a cacheable tier rather than assume a rate on your behalf.
|
|
1936
|
+
*/
|
|
1937
|
+
prefix?: {
|
|
1938
|
+
stableTokens: number;
|
|
1939
|
+
hitRate: number;
|
|
1940
|
+
};
|
|
1941
|
+
/**
|
|
1942
|
+
* What running the compressor costs, in equivalent tokens (default 0, which
|
|
1943
|
+
* fits a local deterministic compressor). A compressor that is itself another
|
|
1944
|
+
* model call is not free, and unaccounted spend shows up nowhere else.
|
|
1945
|
+
*/
|
|
1946
|
+
compressorCostTokens?: number;
|
|
1947
|
+
/** Do not compress contexts below this many tokens (default 2000). */
|
|
1948
|
+
minimumContextTokens?: number;
|
|
1949
|
+
/** Do not apply a compression that saves fewer net tokens than this (default 200). */
|
|
1950
|
+
minimumNetTokens?: number;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1349
1953
|
/**
|
|
1350
1954
|
* Provider-neutral generation interface. Every provider (OpenAI-compatible,
|
|
1351
1955
|
* Anthropic-compatible, Google-compatible, self-hosted) is adapted to this
|
|
@@ -1362,17 +1966,65 @@ interface ProviderRequest {
|
|
|
1362
1966
|
seed?: number;
|
|
1363
1967
|
/** Backend id selected by the constraints layer, for observability. */
|
|
1364
1968
|
constraintBackend?: string;
|
|
1969
|
+
/**
|
|
1970
|
+
* How many LEADING messages form the stable, cacheable prefix — the part the
|
|
1971
|
+
* provider may reuse across requests. `undefined` claims nothing and adapters
|
|
1972
|
+
* mark no cache breakpoint.
|
|
1973
|
+
*
|
|
1974
|
+
* Adapters that need EXPLICIT breakpoints (Anthropic) place them at this
|
|
1975
|
+
* boundary; adapters that cache automatically (OpenAI) may ignore it. The
|
|
1976
|
+
* count is a claim about byte-for-byte stability, so a caller that rewrites an
|
|
1977
|
+
* earlier turn must lower it.
|
|
1978
|
+
*/
|
|
1979
|
+
stablePrefixMessages?: number;
|
|
1980
|
+
/**
|
|
1981
|
+
* Routing hint so requests sharing a prefix land on the same cache shard
|
|
1982
|
+
* (OpenAI `prompt_cache_key`). It is a hint, never an identity: it must not
|
|
1983
|
+
* carry tenant or user data, because it does not isolate anything.
|
|
1984
|
+
*/
|
|
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;
|
|
1365
1999
|
}
|
|
1366
2000
|
interface ProviderUsage {
|
|
1367
2001
|
inputTokens: number;
|
|
1368
2002
|
outputTokens: number;
|
|
1369
2003
|
/** Provider-reported prefix/KV-cached input tokens (ledger 4), if any. */
|
|
1370
2004
|
cachedInputTokens?: number;
|
|
2005
|
+
/**
|
|
2006
|
+
* Input tokens billed at a PREMIUM to WRITE the provider's prefix cache
|
|
2007
|
+
* (Anthropic `cache_creation_input_tokens`: 1.25x the normal rate).
|
|
2008
|
+
*
|
|
2009
|
+
* This is a COST, not a discount, and it is reported separately for exactly
|
|
2010
|
+
* that reason: `cachedInputTokens` is a saving to publish in ledger 4, this
|
|
2011
|
+
* is the price paid to obtain it. Folding the two together would report a
|
|
2012
|
+
* bill INCREASE as a saving.
|
|
2013
|
+
*/
|
|
2014
|
+
cacheWriteInputTokens?: number;
|
|
1371
2015
|
}
|
|
1372
2016
|
interface ProviderResponse {
|
|
1373
2017
|
text: string;
|
|
1374
2018
|
usage: ProviderUsage;
|
|
1375
|
-
/**
|
|
2019
|
+
/**
|
|
2020
|
+
* Whether the provider enforced the schema natively.
|
|
2021
|
+
*
|
|
2022
|
+
* An explicit `false` is a claim about the guarantee that was obtained, not a
|
|
2023
|
+
* detail: the pipeline reads it and reports `constraints.backend: "none"`, so
|
|
2024
|
+
* an adapter that had to ask for JSON in the prompt (for instance because the
|
|
2025
|
+
* model rejects `response_format`) must set it. Leaving it undefined asserts
|
|
2026
|
+
* nothing and keeps the planned backend.
|
|
2027
|
+
*/
|
|
1376
2028
|
structuredNatively?: boolean;
|
|
1377
2029
|
/**
|
|
1378
2030
|
* Por qué paró de generar el modelo. `"length"` significa que se quedó sin
|
|
@@ -1395,6 +2047,30 @@ interface ProviderResponse {
|
|
|
1395
2047
|
* all the way to the caller.
|
|
1396
2048
|
*/
|
|
1397
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;
|
|
1398
2074
|
raw?: unknown;
|
|
1399
2075
|
}
|
|
1400
2076
|
interface Provider {
|
|
@@ -1410,6 +2086,22 @@ interface Provider {
|
|
|
1410
2086
|
* demonstrating the constrained-decoding contract end to end. Token usage is
|
|
1411
2087
|
* derived from the heuristic estimator so SavingsReport numbers are meaningful
|
|
1412
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`.
|
|
1413
2105
|
*/
|
|
1414
2106
|
interface MockProviderOptions {
|
|
1415
2107
|
/** Fixed text to return when no schema is present. */
|
|
@@ -1436,6 +2128,13 @@ declare function createMockProvider(options?: MockProviderOptions): Provider;
|
|
|
1436
2128
|
* with a JSON schema when a contract is present, which is the provider-native
|
|
1437
2129
|
* constrained-decoding path preferred by the layer.
|
|
1438
2130
|
*
|
|
2131
|
+
* Models that predate structured outputs reject `response_format` with a 400. The
|
|
2132
|
+
* adapter degrades for those: it repeats the request once with the schema carried
|
|
2133
|
+
* in the prompt instead, remembers the model so later requests skip the rejected
|
|
2134
|
+
* attempt, and reports `structuredNatively: false` so the savings report states
|
|
2135
|
+
* `constraints.backend: "none"` rather than claiming a guarantee the provider
|
|
2136
|
+
* never gave.
|
|
2137
|
+
*
|
|
1439
2138
|
* Network calls use global `fetch` (Node >= 18) so there is no SDK dependency.
|
|
1440
2139
|
*/
|
|
1441
2140
|
interface OpenAICompatibleOptions {
|
|
@@ -1475,8 +2174,74 @@ declare class OpenAICompatibleProvider implements Provider {
|
|
|
1475
2174
|
readonly id: string;
|
|
1476
2175
|
private readonly opts;
|
|
1477
2176
|
private readonly fetchImpl;
|
|
2177
|
+
/** Models of this endpoint already known to reject `response_format`. */
|
|
2178
|
+
private readonly sinResponseFormat;
|
|
1478
2179
|
constructor(opts: OpenAICompatibleOptions);
|
|
1479
2180
|
generate(req: ProviderRequest): Promise<ProviderResponse>;
|
|
2181
|
+
/** Build the request body and post it. `nativo` selects the `response_format` path. */
|
|
2182
|
+
private enviar;
|
|
2183
|
+
/** Translate a 2xx endpoint response into the provider-neutral shape. */
|
|
2184
|
+
private leer;
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
/**
|
|
2188
|
+
* Anthropic Messages API adapter.
|
|
2189
|
+
*
|
|
2190
|
+
* Speaks `POST /v1/messages`: the system prompt travels in its own top-level
|
|
2191
|
+
* `system` field (not as a message), tools are `{name, description,
|
|
2192
|
+
* input_schema}` with no `function` wrapper, and prefix caching is requested
|
|
2193
|
+
* with EXPLICIT `cache_control` breakpoints — Anthropic does not cache on its
|
|
2194
|
+
* own. A cached read is billed at 0.1x the input rate, which is the largest
|
|
2195
|
+
* single lever on the input side of a customer's bill.
|
|
2196
|
+
*
|
|
2197
|
+
* Network calls use global `fetch` (Node >= 18) so there is no SDK dependency.
|
|
2198
|
+
*/
|
|
2199
|
+
/** Render order of the request, which is also the order the cache prefix grows in. */
|
|
2200
|
+
interface AnthropicOptions {
|
|
2201
|
+
apiKey?: string;
|
|
2202
|
+
/** Defaults to the public API. Override for a gateway or a proxy. */
|
|
2203
|
+
baseUrl?: string;
|
|
2204
|
+
/** Wire version pinned in the `anthropic-version` header. */
|
|
2205
|
+
apiVersion?: string;
|
|
2206
|
+
/** Extra headers (e.g. for a gateway). */
|
|
2207
|
+
headers?: Record<string, string>;
|
|
2208
|
+
/** Injectable fetch for testing. */
|
|
2209
|
+
fetchImpl?: typeof fetch;
|
|
2210
|
+
/**
|
|
2211
|
+
* `max_tokens` is REQUIRED by this API — unlike the OpenAI shape, where it is
|
|
2212
|
+
* optional. A request without one is a 400, so the adapter has to supply a
|
|
2213
|
+
* value. It is deliberately generous: a low default would truncate answers
|
|
2214
|
+
* and the truncation would look like a model failure rather than an adapter
|
|
2215
|
+
* default.
|
|
2216
|
+
*/
|
|
2217
|
+
defaultMaxOutputTokens?: number;
|
|
2218
|
+
/**
|
|
2219
|
+
* When true (recommended for measurement), a response whose `usage` block is
|
|
2220
|
+
* missing its counters is treated as a HARD ERROR instead of being coerced to
|
|
2221
|
+
* 0 tokens. An absent usage payload otherwise becomes `actual cost = 0` → a
|
|
2222
|
+
* fabricated 100% saving.
|
|
2223
|
+
*/
|
|
2224
|
+
requireUsage?: boolean;
|
|
2225
|
+
/**
|
|
2226
|
+
* How many `cache_control` breakpoints the adapter may spend.
|
|
2227
|
+
*
|
|
2228
|
+
* The API caps them at 4 per request. The adapter never needs more than 2
|
|
2229
|
+
* (end of system, end of the stable message prefix) and the cap is exposed so
|
|
2230
|
+
* a gateway with a stricter limit can lower it. Raising it above 4 is
|
|
2231
|
+
* pointless: the extra breakpoints are rejected by the API, not ignored.
|
|
2232
|
+
*/
|
|
2233
|
+
maxCacheBreakpoints?: number;
|
|
2234
|
+
}
|
|
2235
|
+
declare class AnthropicProvider implements Provider {
|
|
2236
|
+
readonly id: string;
|
|
2237
|
+
private readonly opts;
|
|
2238
|
+
private readonly fetchImpl;
|
|
2239
|
+
constructor(opts?: AnthropicOptions);
|
|
2240
|
+
generate(req: ProviderRequest): Promise<ProviderResponse>;
|
|
2241
|
+
private construirCuerpo;
|
|
2242
|
+
private marcarCache;
|
|
2243
|
+
/** Traduce una respuesta 2xx a la forma neutra del layer. */
|
|
2244
|
+
private leer;
|
|
1480
2245
|
}
|
|
1481
2246
|
|
|
1482
2247
|
/**
|
|
@@ -1500,7 +2265,19 @@ declare class TraceCollector {
|
|
|
1500
2265
|
* unlicensed, rather than silently serving requests for free.
|
|
1501
2266
|
*/
|
|
1502
2267
|
declare class LicenseError extends Error {
|
|
1503
|
-
/**
|
|
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
|
+
*/
|
|
1504
2281
|
readonly status?: string;
|
|
1505
2282
|
constructor(message: string, status?: string);
|
|
1506
2283
|
}
|
|
@@ -1509,10 +2286,10 @@ declare class LicenseError extends Error {
|
|
|
1509
2286
|
* Offline license verification for the paid SDK.
|
|
1510
2287
|
*
|
|
1511
2288
|
* A license is a compact signed token `base64url(header).base64url(payload).
|
|
1512
|
-
* base64url(sig)
|
|
1513
|
-
*
|
|
1514
|
-
*
|
|
1515
|
-
*
|
|
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
|
|
1516
2293
|
* `@noble/curves` (pure JS) + Web primitives (`atob`, `TextEncoder`) rather than
|
|
1517
2294
|
* `node:crypto`, so the shared entry point still compiles into the browser
|
|
1518
2295
|
* demo bundle. The private signing key never leaves the control plane; only the
|
|
@@ -1610,8 +2387,8 @@ declare function deriveStatus(payload: {
|
|
|
1610
2387
|
grace_days: number;
|
|
1611
2388
|
}, nowSeconds: number, revoked?: ReadonlySet<string>): LicenseStatus;
|
|
1612
2389
|
/**
|
|
1613
|
-
* Verify + apply the grace policy → a resolved status. Mirrors
|
|
1614
|
-
*
|
|
2390
|
+
* Verify + apply the grace policy → a resolved status. Mirrors what the issuing
|
|
2391
|
+
* control plane resolves for the same token. Bad signature / malformed
|
|
1615
2392
|
* → invalid; otherwise the status comes from {@link deriveStatus}.
|
|
1616
2393
|
*/
|
|
1617
2394
|
declare function resolveLicense(token: string, publicKey: string | LicenseKeyset, opts?: {
|
|
@@ -1874,6 +2651,13 @@ interface UsageEmitterOptions {
|
|
|
1874
2651
|
* exactamente lo mismo que se ve cuando no hay tráfico—.
|
|
1875
2652
|
*/
|
|
1876
2653
|
onAuthError?: (status: number) => void;
|
|
2654
|
+
/**
|
|
2655
|
+
* Silence the console warnings. They are ON by default, exactly as in the
|
|
2656
|
+
* revocation poller: metering is best-effort, so a broken ingest breaks
|
|
2657
|
+
* nothing visible — usage simply stops arriving, silently, and the customer's
|
|
2658
|
+
* dashboard stays empty forever.
|
|
2659
|
+
*/
|
|
2660
|
+
silenceWarnings?: boolean;
|
|
1877
2661
|
}
|
|
1878
2662
|
/**
|
|
1879
2663
|
* Buffers counts-only usage and flushes it to the control plane. DETECTIVE
|
|
@@ -1893,6 +2677,13 @@ declare class UsageEmitter {
|
|
|
1893
2677
|
private ci;
|
|
1894
2678
|
private timer;
|
|
1895
2679
|
private flushing;
|
|
2680
|
+
/**
|
|
2681
|
+
* CONSECUTIVE failed flushes. Warn on the third and then every twelfth: one
|
|
2682
|
+
* lost batch is normal, a whole hour is not. Same cadence as the revocation
|
|
2683
|
+
* poller on purpose — two different rhythms for the same class of failure
|
|
2684
|
+
* only confuse whoever reads the logs.
|
|
2685
|
+
*/
|
|
2686
|
+
private fallosSeguidos;
|
|
1896
2687
|
constructor(options: UsageEmitterOptions);
|
|
1897
2688
|
/** Set the resolved seat identity (derivation is async; records emitted before
|
|
1898
2689
|
* it resolves are stamped at flush time, so none are lost). */
|
|
@@ -1908,6 +2699,19 @@ declare class UsageEmitter {
|
|
|
1908
2699
|
* since retrying a rejected payload would loop forever.
|
|
1909
2700
|
*/
|
|
1910
2701
|
flush(): Promise<void>;
|
|
2702
|
+
/**
|
|
2703
|
+
* THE INGEST HAS BEEN FAILING FOR A WHILE AND NOBODY NOTICED.
|
|
2704
|
+
*
|
|
2705
|
+
* Metering is best-effort by design — it must never take down the customer's
|
|
2706
|
+
* request — and that turns every failure into SILENT data loss. Without this,
|
|
2707
|
+
* the only symptom is an empty dashboard, which is indistinguishable from
|
|
2708
|
+
* "you haven't sent anything yet": the symptom points the opposite way from
|
|
2709
|
+
* the cause.
|
|
2710
|
+
*
|
|
2711
|
+
* Warns on the third consecutive failure and then every twelfth. Not the
|
|
2712
|
+
* first — a lost request is normal — and not every one, which becomes noise.
|
|
2713
|
+
*/
|
|
2714
|
+
private avisarDeFallo;
|
|
1911
2715
|
private requeue;
|
|
1912
2716
|
private toWire;
|
|
1913
2717
|
}
|
|
@@ -1953,8 +2757,24 @@ interface SavingsLayerConfig {
|
|
|
1953
2757
|
schemaRegistry?: SchemaRegistry;
|
|
1954
2758
|
constraintBackends?: ConstraintBackend[];
|
|
1955
2759
|
compressor?: Compressor;
|
|
2760
|
+
/**
|
|
2761
|
+
* The numbers behind "compress only when it pays for itself" — in money, not
|
|
2762
|
+
* in tokens.
|
|
2763
|
+
*
|
|
2764
|
+
* All optional, and the default never invents a saving: the layer compresses
|
|
2765
|
+
* when the net token saving justifies it, and holds back when compressing
|
|
2766
|
+
* would push the prompt below the provider's cacheable minimum — where fewer
|
|
2767
|
+
* tokens can mean a larger bill. Declare {@link CompressionEconomics.prefix}
|
|
2768
|
+
* with your own measurements to unlock the full decision in both directions.
|
|
2769
|
+
*/
|
|
2770
|
+
compressionEconomics?: CompressionEconomics;
|
|
1956
2771
|
retriever?: Retriever;
|
|
1957
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;
|
|
1958
2778
|
/**
|
|
1959
2779
|
* Signed BiVelio license token (from your paid subscription). This is the
|
|
1960
2780
|
* primary way to license the SDK: `createSavingsLayer` verifies it offline
|
|
@@ -1972,10 +2792,11 @@ interface SavingsLayerConfig {
|
|
|
1972
2792
|
*/
|
|
1973
2793
|
licensePublicKey?: string | LicenseKeyset;
|
|
1974
2794
|
/**
|
|
1975
|
-
* Pre-built license gate.
|
|
1976
|
-
*
|
|
1977
|
-
*
|
|
1978
|
-
*
|
|
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}.
|
|
1979
2800
|
*/
|
|
1980
2801
|
licenseGate?: LicenseGate;
|
|
1981
2802
|
/**
|
|
@@ -2106,11 +2927,73 @@ interface SavingsLayerConfig {
|
|
|
2106
2927
|
* in tests/benchmarks that want internal errors to surface.
|
|
2107
2928
|
*/
|
|
2108
2929
|
failOpen?: boolean;
|
|
2930
|
+
/**
|
|
2931
|
+
* Silence BV-SALA's console warnings. They are ON by default, and they only
|
|
2932
|
+
* ever report a condition whose symptom is otherwise invisible: a saving knob
|
|
2933
|
+
* asked for but not wired up, usage that stops reaching the control plane, a
|
|
2934
|
+
* revocation list going stale. Nothing here throws and nothing here changes
|
|
2935
|
+
* what the layer does — turning it on only removes the console noise, so set
|
|
2936
|
+
* it when you have your own observability. Applies to the pipeline, the usage
|
|
2937
|
+
* emitter and the revocation poller alike.
|
|
2938
|
+
*/
|
|
2939
|
+
silenceWarnings?: boolean;
|
|
2109
2940
|
/** Injectable clock for deterministic tests. */
|
|
2110
2941
|
now?: () => number;
|
|
2111
2942
|
/** Injectable id generator for deterministic tests. */
|
|
2112
2943
|
generateId?: () => string;
|
|
2113
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
|
+
}
|
|
2114
2997
|
declare const DEFAULT_COST_MODEL: CostModel;
|
|
2115
2998
|
|
|
2116
2999
|
/** Fully-resolved, non-optional dependency set the pipeline runs against. */
|
|
@@ -2147,12 +3030,18 @@ interface SavingsLayer {
|
|
|
2147
3030
|
* Create a savings layer. The only hard requirement is a `provider`; a default
|
|
2148
3031
|
* heuristic tokenizer and in-memory implementations of every other component
|
|
2149
3032
|
* are wired automatically. Replace any of them for production (Redis exact
|
|
2150
|
-
* cache,
|
|
3033
|
+
* cache, a vector-backed semantic cache, a real tokenizer, a compressor sidecar,
|
|
2151
3034
|
* XGrammar/llguidance backends, etc.).
|
|
2152
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
|
+
*
|
|
2153
3040
|
* @example
|
|
2154
|
-
*
|
|
2155
|
-
*
|
|
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({
|
|
2156
3045
|
* model: "openai/gpt-4o-mini",
|
|
2157
3046
|
* messages: [{ role: "user", content: "What is 2 + 2 * 3?" }],
|
|
2158
3047
|
* });
|
|
@@ -2185,6 +3074,7 @@ declare function createSavingsLayer(config: Omit<SavingsLayerConfig, "tokenizer"
|
|
|
2185
3074
|
* on the customer's machine. `test/version-declarada.test.ts` ties this
|
|
2186
3075
|
* constant to package.json so the two can never drift.
|
|
2187
3076
|
*/
|
|
2188
|
-
declare const BV_SALA_VERSION = "0.2.2";
|
|
2189
3077
|
|
|
2190
|
-
|
|
3078
|
+
declare const BV_SALA_VERSION = "0.3.1";
|
|
3079
|
+
|
|
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 };
|