@bivelio/savings-layer 0.2.2 → 0.3.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/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,19 @@ 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;
78
81
  /**
79
82
  * Lo que de verdad hizo la compresión, para publicarlo en
80
83
  * `SavingsReport.compression`. El campo estaba declarado en el contrato y no lo
@@ -113,6 +116,53 @@ declare class SavingsLedger {
113
116
  };
114
117
  }
115
118
 
119
+ /**
120
+ * LIST PRICES PER MODEL.
121
+ *
122
+ * The layer reports savings in money, and money needs a price. This is the
123
+ * built-in catalogue of published list prices, so the dollar figures mean
124
+ * something without any configuration.
125
+ *
126
+ * ⚠️ A WRONG PRICE HERE IS WORSE THAN NO CATALOGUE. A model that is ABSENT falls
127
+ * back to the illustrative default AND warns, so the customer knows that column
128
+ * is not their invoice. A model that is PRESENT with the wrong price silences
129
+ * that warning and hands over a confident, wrong number. So only models whose
130
+ * published price is known with certainty belong here, and {@link PRECIOS_AL_DIA}
131
+ * records when they were last checked.
132
+ *
133
+ * Prices are USD per 1,000 tokens — the unit of `CostModel` — even though
134
+ * providers publish per million: 1M to 1K is a division by a thousand.
135
+ *
136
+ * `costModels` on the layer config ALWAYS wins over this. A customer with volume
137
+ * discounts, negotiated rates or a different region sets their own and this
138
+ * catalogue stays out of the way.
139
+ */
140
+ /** When these prices were last checked against the providers' public rates. */
141
+ declare const PRECIOS_AL_DIA = "2026-08-13";
142
+ /**
143
+ * Key = the model id as the provider publishes it, without a vendor prefix.
144
+ * Lookup tries the exact id, then the id without its provider prefix, then the
145
+ * LONGEST matching prefix — so `gpt-4o-2024-08-06` inherits `gpt-4o` without
146
+ * having to enumerate every dated snapshot.
147
+ */
148
+ declare const CATALOGO: Readonly<Record<string, CostModel>>;
149
+ /**
150
+ * The list price for a model, or `undefined` when it is not in the catalogue.
151
+ *
152
+ * @param model the id as the caller passes it, with or without a vendor prefix.
153
+ */
154
+ declare function precioDeLista(model: string): CostModel | undefined;
155
+
156
+ /**
157
+ * Environment-agnostic UUID generation.
158
+ * ---------------------------------------------------------------------------
159
+ * Trace IDs only need to be unique, not cryptographically unpredictable. We
160
+ * prefer the platform `crypto.randomUUID` (present in modern Node, browsers,
161
+ * Deno and edge runtimes) and fall back to a v4-shaped id when it is absent, so
162
+ * BV-SALA runs everywhere without a `node:crypto` import that breaks bundlers.
163
+ */
164
+ declare function randomId(): string;
165
+
116
166
  /**
117
167
  * Core contracts for BV-SALA (bivelio-savings-layer).
118
168
  *
@@ -126,8 +176,8 @@ declare class SavingsLedger {
126
176
  * - provider_cached_tokens tokens still in the prompt that the provider may
127
177
  * bill or process differently (prefix/KV caching).
128
178
  *
129
- * A cache hit eliminates a whole call. TOON re-encodes a structured payload.
130
- * LLMLingua removes redundant words. A provider prefix cache makes tokens
179
+ * A cache hit eliminates a whole call. A compact encoding re-encodes a structured payload.
180
+ * A context compressor removes redundant words. A provider prefix cache makes tokens
131
181
  * cheaper without removing them. Mixing these produces misleading metrics.
132
182
  */
133
183
  /** Roles understood by the provider-neutral chat interface. */
@@ -187,7 +237,7 @@ interface OutputContract {
187
237
  version?: string;
188
238
  }
189
239
  /** Serialization formats the format-selector can choose between. */
190
- type SerializationFormat = "json-compact" | "toon" | "csv";
240
+ type SerializationFormat = "json-compact" | "compact-rows" | "csv";
191
241
  interface RetrievalOptions {
192
242
  mode: "off" | "auto" | "single" | "multi";
193
243
  maxRounds?: number;
@@ -201,6 +251,61 @@ interface SavingsToggles {
201
251
  singleflight?: boolean;
202
252
  deterministicResolvers?: boolean;
203
253
  router?: boolean;
254
+ /**
255
+ * OPT-IN. Send the provider a compacted form of your output contract — short
256
+ * property keys, short enum codes — so the model spends fewer tokens saying
257
+ * the same thing, and expand the reply back to YOUR shape before it is
258
+ * validated, cached or returned. What you receive in `data`, `text` and
259
+ * `rendered` is identical either way.
260
+ *
261
+ * It is applied only when the layer can prove the expansion is exact for that
262
+ * schema and only when it counts as a net token win on both sides (the
263
+ * compact contract itself travels as input). Otherwise your schema is sent
264
+ * unchanged. Off by default because it changes what the provider sees, and
265
+ * whether a model answers as WELL from a compact contract is a question only
266
+ * an A/B on your own traffic can settle. Check `report.traces` for what it
267
+ * decided and why.
268
+ */
269
+ outputCodebook?: boolean;
270
+ /**
271
+ * OPT-IN, and it changes the latency contract: where it applies, one request
272
+ * becomes TWO provider calls, one after the other.
273
+ *
274
+ * With a large tool catalogue the full JSON schema of every candidate travels
275
+ * on every request, even though the model will use one tool or none. With this
276
+ * on, the layer may first send lightweight descriptors (name + one line, no
277
+ * `parameters`), read which tools the model asks for, and only then send those
278
+ * full schemas.
279
+ *
280
+ * It is applied only when the token arithmetic for THIS request, priced with
281
+ * your cost model, says the second call pays for itself with margin — so a
282
+ * small catalogue silently keeps the single-call path.
283
+ *
284
+ * THE MARGIN IS NOT A GUARANTEE THAT EVERY REQUEST GETS CHEAPER. When the
285
+ * model names no tool in the first call, the request still goes out with every
286
+ * schema and the first call is pure added cost: that request costs MORE, and
287
+ * the report says so (`cost.netSavings` goes negative). The margin required is
288
+ * exactly the cost of that outcome, so the pattern breaks even while it
289
+ * happens on at most one request in two. If your traffic asks for a tool less
290
+ * often than that — a chat assistant with a big catalogue answers most turns
291
+ * directly — leave this off. Only an A/B against your own traffic settles it.
292
+ *
293
+ * IT ALSO GIVES UP THE PROVIDER'S PREFIX CACHE, on every request, not just the
294
+ * ones where it applies. Tools render FIRST in a provider request, and with
295
+ * this on, which schemas travel is decided by a probe whose answer is not
296
+ * deterministic — so the front of the prompt varies between requests that
297
+ * would otherwise share a cacheable prefix. Marking one would pay the write
298
+ * premium (1.25x on Anthropic) for a prefix that cannot hit. If your prompts
299
+ * carry a long fixed system header and your traffic repeats it, that discount
300
+ * is very likely worth more than this lever: measure both before choosing.
301
+ * `report.traces` says which one is off and why.
302
+ *
303
+ * When the second call happened, `report.calls.actualLlm` is 2. The extra
304
+ * call's tokens are counted as spend and never credited to your baseline.
305
+ * Write and destructive tools always travel with their full schema, so no
306
+ * action can be lost to a discovery step.
307
+ */
308
+ toolDiscovery?: boolean;
204
309
  }
205
310
  interface RequestContext {
206
311
  tenantId?: string;
@@ -266,6 +371,42 @@ interface SavingsResult<T = unknown> {
266
371
  report: SavingsReport;
267
372
  }
268
373
  type CacheOutcome = "miss" | "exact" | "semantic" | "singleflight" | "deterministic";
374
+ /**
375
+ * The quality claims this layer is able to DEMONSTRATE for the answer it served.
376
+ *
377
+ * A boolean cannot carry this. `constraints.structurallyValid` puts an answer
378
+ * computed by a deterministic resolver — exact by construction — in the same box
379
+ * as one the model wrote while nobody checked anything but its shape. Those two
380
+ * are the difference between "this is the result" and "this is what the model
381
+ * said", and a product that claims savings owes the reader that distinction.
382
+ *
383
+ * THE RULE BEHIND THIS VOCABULARY: never report "no loss" on the strength of
384
+ * cosine similarity, BLEU, ROUGE or another model's judgement. Those scores stay
385
+ * high while a figure, a negation or an exception quietly disappears — precisely
386
+ * the fields that decide a legal or financial document. A claim of equivalence
387
+ * rests on a deterministic gate, or it is not made.
388
+ *
389
+ * - `exact` exact by construction: a deterministic resolver computed
390
+ * the answer, or an exact-cache hit replayed the answer
391
+ * already produced for this very same request.
392
+ * - `task_equivalent` not byte-identical, but it cleared a deterministic gate
393
+ * before being served — numbers and dates as a multiset,
394
+ * negation parity and a key-entity floor all had to agree,
395
+ * on top of any similarity score.
396
+ * - `not_verified` the model was called and nothing beyond the shape of the
397
+ * answer was checked. The honest default, and the common case.
398
+ * - `rejected` what was served does not satisfy its output contract, and
399
+ * a repair either failed or would have been a wasted call.
400
+ * The answer is still returned: this is the layer's verdict
401
+ * on it, not a refusal to hand it over.
402
+ *
403
+ * The list is deliberately short. A status nobody produces is a promise the
404
+ * reader cannot check, so levers that do not exist yet bring their own status
405
+ * the day they can demonstrate it. Read this as an OPEN set — keep a default
406
+ * branch, exactly as with a licence status.
407
+ */
408
+ declare const QUALITY_STATUSES: readonly ["exact", "task_equivalent", "not_verified", "rejected"];
409
+ type QualityStatus = (typeof QUALITY_STATUSES)[number];
269
410
  interface SavingsReport {
270
411
  requestId: string;
271
412
  model: string;
@@ -328,8 +469,27 @@ interface SavingsReport {
328
469
  */
329
470
  structurallyValid?: boolean;
330
471
  };
472
+ /**
473
+ * How the served answer earned its place, in one word. Optional and additive:
474
+ * a report built by an older SDK simply does not carry it, so treat an absent
475
+ * value as "unknown", never as a pass.
476
+ *
477
+ * It only ever DEGRADES what the rest of the report claims. An answer that
478
+ * fails its output contract is `rejected` however it was produced — a resolver
479
+ * that computes the right number for the wrong contract does not get to keep
480
+ * the `exact` label.
481
+ */
482
+ qualityStatus?: QualityStatus;
331
483
  quality: {
332
484
  taskPassed?: boolean;
485
+ /**
486
+ * Similarity score of the retained answer, when one was computed.
487
+ *
488
+ * A number here is NOT evidence of equivalence: see {@link QualityStatus}.
489
+ * Cosine (and BLEU, ROUGE, or an LLM judge) can sit above 0.95 across a
490
+ * changed figure or a dropped negation, so it informs a decision — it never
491
+ * licenses a "no loss" claim.
492
+ */
333
493
  semanticScore?: number;
334
494
  freshnessPassed?: boolean;
335
495
  };
@@ -410,7 +570,7 @@ interface TraceEvent {
410
570
  * be re-explained in free-form natural language at every hop. BV-SALA replaces
411
571
  * that with a compact, typed handoff object. Each consumer receives a
412
572
  * projection of the state, not the whole conversation. The serializer then
413
- * decides whether to send it as compact JSON or TOON.
573
+ * decides whether to send it as compact JSON or compact rows.
414
574
  */
415
575
  interface HandoffFact {
416
576
  id: string;
@@ -460,7 +620,7 @@ interface TokenEstimator {
460
620
  * El heurístico vale para RANKEAR formatos candidatos (le basta con acertar el
461
621
  * orden). No vale para restar tokens entre formatos distintos: cobra cada
462
622
  * símbolo suelto como un token entero, así que infla el JSON denso en símbolos
463
- * mucho más que el TOON, y la diferencia se acredita como ahorro inexistente.
623
+ * mucho más que el the compact encoder, y la diferencia se acredita como ahorro inexistente.
464
624
  */
465
625
  readonly exact: boolean;
466
626
  /** Estimate the number of tokens in a string. */
@@ -517,18 +677,9 @@ declare const defaultTokenizerRegistry: TokenizerRegistry;
517
677
  * contamos aquí. Con el estimador heurístico esos dos sumandos vienen de reglas
518
678
  * distintas, y la diferencia NO es ruido: el heurístico cobra cada símbolo suelto
519
679
  * como un token entero, así que infla el JSON (40% de símbolos) mucho más que el
520
- * TOON (16%). Medido en el escenario de la tabla: publicaba un baseline 3,68x el
521
- * naive real, o sea un +47,4% de ahorro donde el proveedor midió −49,6%.
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.
680
+ * An estimator that is not calibrated for the destination model can miss by
681
+ * enough to publish a baseline several times the real one, which would credit a
682
+ * saving that never existed. That is why `basis` degrades instead of claiming.
532
683
  */
533
684
 
534
685
  /** Lo que necesitamos de gpt-tokenizer. Tipado aquí para no depender de sus tipos. */
@@ -588,7 +739,11 @@ interface CanonicalRequest {
588
739
  model: string;
589
740
  modelFamily: string;
590
741
  messages: ChatMessage[];
591
- /** Whitespace/case-normalized text of the user-visible turns. */
742
+ /**
743
+ * Text of the user-visible turns, normalized so that trivial near-duplicates
744
+ * collide: Unicode form, case, whitespace, typographic marks and sentence
745
+ * punctuation. Numbers, dates, quantities and negations are preserved exactly.
746
+ */
592
747
  normalizedText: string;
593
748
  systemPromptHash: string;
594
749
  toolsetHash: string;
@@ -616,6 +771,14 @@ interface CanonicalRequest {
616
771
  tokenizer: string | undefined;
617
772
  original: SavingsRequest;
618
773
  }
774
+ /**
775
+ * Normalized form of a turn's text, as it enters the cache key.
776
+ *
777
+ * Folded away: Unicode form, case, whitespace, typographic marks, Latin
778
+ * diacritics and sentence punctuation. Preserved exactly: numbers, decimal and
779
+ * thousands separators, times, dates, units, negations, math operators and any
780
+ * combining mark outside the Latin alphabet.
781
+ */
619
782
  declare function normalizeText(text: string): string;
620
783
  declare function sha256(input: string): string;
621
784
  /** Stable JSON stringify with sorted keys so hashes are order-independent. */
@@ -821,7 +984,7 @@ declare function cosine(a: number[], b: number[]): number;
821
984
  declare function keyTokensMatch(a: string, b: string): boolean;
822
985
  /**
823
986
  * In-memory reference implementation. A production build would back this with a
824
- * real vector store (pgvector, Qdrant, etc.) and metadata filters pushed down
987
+ * real vector store and metadata filters pushed down
825
988
  * to the store. The verification logic — the part that makes it *safe* — stays
826
989
  * identical regardless of the backend.
827
990
  */
@@ -936,6 +1099,17 @@ interface ToolSelection {
936
1099
  tools: ToolDescriptor[];
937
1100
  droppedIds: string[];
938
1101
  reason: string;
1102
+ /**
1103
+ * Whether the SET of tools is determined by something stable across requests,
1104
+ * rather than by this request's text.
1105
+ *
1106
+ * It is not cosmetic. Tools render FIRST in a provider request (tools ->
1107
+ * system -> messages), so a tool set that changes with the question moves the
1108
+ * very front of the prompt and invalidates the whole prefix cache behind it.
1109
+ * A caller whose prompt caching silently never hits, on traffic with tools, is
1110
+ * usually looking at this.
1111
+ */
1112
+ setIsStable: boolean;
939
1113
  }
940
1114
  interface ToolSelectorOptions {
941
1115
  /** Max tools to include (schema-token budget guard). */
@@ -1072,7 +1246,7 @@ interface Serializer {
1072
1246
  encode(value: unknown): string;
1073
1247
  /**
1074
1248
  * The instruction text a model needs to interpret this format. For JSON this
1075
- * is empty; for TOON/CSV it is the per-request cost that must be amortized
1249
+ * is empty; for row/CSV it is the per-request cost that must be amortized
1076
1250
  * before the format is worthwhile.
1077
1251
  */
1078
1252
  instruction(value: unknown): string;
@@ -1090,60 +1264,60 @@ interface Serializer {
1090
1264
  * Compact JSON: the lossless baseline every other format is measured against.
1091
1265
  * No indentation, no incidental whitespace. This is frequently the *winner* for
1092
1266
  * deeply nested or non-uniform data, which is exactly why it is the baseline and
1093
- * TOON is only ever a challenger.
1267
+ * The compact encoder is only ever a challenger.
1094
1268
  */
1095
1269
  declare const jsonCompactSerializer: Serializer;
1096
1270
 
1097
1271
  /**
1098
- * TOON (Token-Oriented Object Notation) adapter.
1272
+ * Compact row encoder.
1099
1273
  *
1100
- * TOON's strength is UNIFORM ARRAYS OF FLAT OBJECTS: it declares the column
1274
+ * Its strength is UNIFORM ARRAYS OF FLAT OBJECTS: it declares the column
1101
1275
  * names once and then emits one row per object, which removes the repeated keys
1102
1276
  * that make JSON expensive for tabular data. On deeply nested or non-uniform
1103
- * data TOON can be *larger* than compact JSON — so this is only ever a candidate
1277
+ * data it can be *larger* than compact JSON — so this is only ever a candidate
1104
1278
  * the selector may pick, never a default.
1105
1279
  *
1106
1280
  * This is an in-house, pinned encoder placed behind the {@link Serializer}
1107
1281
  * 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<->TOON
1282
+ * It emits a well-defined subset and ships a matching decoder so the JSON<->rows
1109
1283
  * round-trip is deterministic and testable.
1110
1284
  */
1111
- interface ToonOptions {
1285
+ interface CompactRowsOptions {
1112
1286
  /** Field delimiter within tabular/inline rows. */
1113
1287
  delimiter?: "," | "\t";
1114
1288
  /** Indentation unit. */
1115
1289
  indent?: string;
1116
1290
  }
1117
- declare function encodeToon(value: unknown, options?: ToonOptions): string;
1291
+ declare function encodeCompactRows(value: unknown, options?: CompactRowsOptions): string;
1118
1292
  /** Return shared column names iff every element is a flat, scalar-valued object with identical keys. */
1119
1293
  declare function uniformColumns(arr: unknown[]): string[] | null;
1120
1294
  /**
1121
- * The instruction a model needs to read TOON. Kept short on purpose — this text
1122
- * is the per-request overhead the selector must amortize before TOON wins.
1295
+ * The instruction a model needs to read the compact encoder. Kept short on purpose — this text
1296
+ * is the per-request overhead the selector must amortize before it wins.
1123
1297
  */
1124
- declare const TOON_INSTRUCTION: string;
1125
- declare function createToonSerializer(options?: ToonOptions): Serializer;
1126
- declare const toonSerializer: Serializer;
1298
+ declare const COMPACT_ROWS_INSTRUCTION: string;
1299
+ declare function createCompactRowsSerializer(options?: CompactRowsOptions): Serializer;
1300
+ declare const compactRowsSerializer: Serializer;
1127
1301
 
1128
1302
  /**
1129
- * TOON decoder for the subset emitted by {@link encodeToon}.
1303
+ * Decoder for the subset emitted by {@link encodeCompactRows}.
1130
1304
  *
1131
1305
  * Supports: scalar fields, nested objects, empty arrays, inline scalar arrays
1132
1306
  * (`key[N]: a,b,c`) and tabular object arrays (`key[N]{c1,c2}:` + rows). This
1133
- * covers the cases the format selector actually chooses TOON for, and makes the
1134
- * JSON<->TOON round-trip deterministic and testable. List-form arrays of nested
1307
+ * covers the cases the format selector actually chooses it for, and makes the
1308
+ * JSON<->rows round-trip deterministic and testable. List-form arrays of nested
1135
1309
  * objects are intentionally out of scope for decoding (the selector never picks
1136
- * TOON for those, since JSON wins there anyway).
1310
+ * it for those, since JSON wins there anyway).
1137
1311
  */
1138
- interface ToonDecodeOptions {
1312
+ interface CompactRowsDecodeOptions {
1139
1313
  delimiter?: "," | "\t";
1140
1314
  indent?: string;
1141
1315
  }
1142
- declare function decodeToon(text: string, options?: ToonDecodeOptions): unknown;
1316
+ declare function decodeCompactRows(text: string, options?: CompactRowsDecodeOptions): unknown;
1143
1317
 
1144
1318
  /**
1145
1319
  * CSV serializer — only valid for genuinely tabular data (a uniform array of
1146
- * flat objects). CSV can be marginally smaller than TOON on pure tables, but it
1320
+ * flat objects). CSV can be marginally smaller than the compact rows on pure tables, but it
1147
1321
  * does NOT preserve the JSON data model in general, so `supports()` is strict.
1148
1322
  */
1149
1323
  declare const csvSerializer: Serializer;
@@ -1151,7 +1325,7 @@ declare const csvSerializer: Serializer;
1151
1325
  /**
1152
1326
  * Format selector.
1153
1327
  *
1154
- * TOON is a candidate, never a default. For each request we count, USING THE
1328
+ * It is a candidate, never a default. For each request we count, USING THE
1155
1329
  * DESTINATION MODEL'S TOKENIZER:
1156
1330
  *
1157
1331
  * tokens(encoded payload) + tokens(instruction to understand it)
@@ -1170,7 +1344,7 @@ interface FormatSelectorOptions {
1170
1344
  minimumSavingsRatio?: number;
1171
1345
  /**
1172
1346
  * Per-(model,format) quality penalty in tokens. Encodes "this model is worse
1173
- * at reading TOON" as an additive cost, learned from production traces.
1347
+ * at reading the compact rows" as an additive cost, learned from production traces.
1174
1348
  */
1175
1349
  qualityPenalty?: (model: string, format: SerializationFormat) => number;
1176
1350
  }
@@ -1274,7 +1448,7 @@ declare function resolveConstraintBackend(model: string, schema: JsonSchema, bac
1274
1448
  declare function providerNativeBackend(isSupported?: (model: string, schema: JsonSchema) => boolean): ConstraintBackend;
1275
1449
 
1276
1450
  /**
1277
- * Context compression (LLMLingua-style), as an OPTIONAL sidecar.
1451
+ * Context compression, as an OPTIONAL sidecar.
1278
1452
  *
1279
1453
  * Two rules make this safe and worthwhile:
1280
1454
  *
@@ -1343,9 +1517,60 @@ declare function shouldCompress(input: CompressionPolicyInput): {
1343
1517
  * worse than no compressor at all.
1344
1518
  */
1345
1519
  declare function withProtectedSpans(inner: Compressor): Compressor;
1346
- /** No-op compressor: the safe default until a real LLMLingua sidecar is wired in. */
1520
+ /** No-op compressor: the safe default until a real compressor sidecar is wired in. */
1347
1521
  declare const noopCompressor: Compressor;
1348
1522
 
1523
+ /** What the layer needs to know about a provider's prefix cache to price a decision. */
1524
+ interface CacheTier {
1525
+ /**
1526
+ * Minimum prompt tokens before the provider caches the prefix at all;
1527
+ * `null` means it never does.
1528
+ *
1529
+ * The built-in tiers assume 1024 for every model. That is an ASSUMPTION, not a
1530
+ * verified figure: real minimums differ per model and family. Set your model's
1531
+ * own minimum here when you know it.
1532
+ */
1533
+ minimumCacheTokens: number | null;
1534
+ /** Price of a cache-read token as a fraction of a normal one (e.g. 0.5, 0.1). */
1535
+ cacheReadMultiplier: number;
1536
+ /** Surcharge for WRITING the cache the first time; some providers bill above 1. */
1537
+ cacheWriteMultiplier: number;
1538
+ }
1539
+
1540
+ /** Tunes when compression is worth its money. Every field is optional. */
1541
+ interface CompressionEconomics {
1542
+ /**
1543
+ * The provider's prefix-cache tier. Derived from the request's model when
1544
+ * omitted; set it to state your model's real cache minimum and multipliers.
1545
+ */
1546
+ cacheTier?: CacheTier;
1547
+ /**
1548
+ * YOUR MEASURED prefix-cache economics: how many prompt tokens are a stable
1549
+ * prefix across requests, and how often that prefix arrives already warm
1550
+ * (`report.tokens.providerCachedInput` is the numerator for the rate).
1551
+ *
1552
+ * Both are required together — a hit rate over an unknown prefix says nothing
1553
+ * about how much money is at stake. Declaring them unlocks the full decision,
1554
+ * including knowingly trading the prefix cache for a smaller prompt when that
1555
+ * is the cheaper side. Left out, the layer refuses to compress a prompt out of
1556
+ * a cacheable tier rather than assume a rate on your behalf.
1557
+ */
1558
+ prefix?: {
1559
+ stableTokens: number;
1560
+ hitRate: number;
1561
+ };
1562
+ /**
1563
+ * What running the compressor costs, in equivalent tokens (default 0, which
1564
+ * fits a local deterministic compressor). A compressor that is itself another
1565
+ * model call is not free, and unaccounted spend shows up nowhere else.
1566
+ */
1567
+ compressorCostTokens?: number;
1568
+ /** Do not compress contexts below this many tokens (default 2000). */
1569
+ minimumContextTokens?: number;
1570
+ /** Do not apply a compression that saves fewer net tokens than this (default 200). */
1571
+ minimumNetTokens?: number;
1572
+ }
1573
+
1349
1574
  /**
1350
1575
  * Provider-neutral generation interface. Every provider (OpenAI-compatible,
1351
1576
  * Anthropic-compatible, Google-compatible, self-hosted) is adapted to this
@@ -1362,17 +1587,52 @@ interface ProviderRequest {
1362
1587
  seed?: number;
1363
1588
  /** Backend id selected by the constraints layer, for observability. */
1364
1589
  constraintBackend?: string;
1590
+ /**
1591
+ * How many LEADING messages form the stable, cacheable prefix — the part the
1592
+ * provider may reuse across requests. `undefined` claims nothing and adapters
1593
+ * mark no cache breakpoint.
1594
+ *
1595
+ * Adapters that need EXPLICIT breakpoints (Anthropic) place them at this
1596
+ * boundary; adapters that cache automatically (OpenAI) may ignore it. The
1597
+ * count is a claim about byte-for-byte stability, so a caller that rewrites an
1598
+ * earlier turn must lower it.
1599
+ */
1600
+ stablePrefixMessages?: number;
1601
+ /**
1602
+ * Routing hint so requests sharing a prefix land on the same cache shard
1603
+ * (OpenAI `prompt_cache_key`). It is a hint, never an identity: it must not
1604
+ * carry tenant or user data, because it does not isolate anything.
1605
+ */
1606
+ promptCacheKey?: string;
1365
1607
  }
1366
1608
  interface ProviderUsage {
1367
1609
  inputTokens: number;
1368
1610
  outputTokens: number;
1369
1611
  /** Provider-reported prefix/KV-cached input tokens (ledger 4), if any. */
1370
1612
  cachedInputTokens?: number;
1613
+ /**
1614
+ * Input tokens billed at a PREMIUM to WRITE the provider's prefix cache
1615
+ * (Anthropic `cache_creation_input_tokens`: 1.25x the normal rate).
1616
+ *
1617
+ * This is a COST, not a discount, and it is reported separately for exactly
1618
+ * that reason: `cachedInputTokens` is a saving to publish in ledger 4, this
1619
+ * is the price paid to obtain it. Folding the two together would report a
1620
+ * bill INCREASE as a saving.
1621
+ */
1622
+ cacheWriteInputTokens?: number;
1371
1623
  }
1372
1624
  interface ProviderResponse {
1373
1625
  text: string;
1374
1626
  usage: ProviderUsage;
1375
- /** Whether the provider enforced the schema natively. */
1627
+ /**
1628
+ * Whether the provider enforced the schema natively.
1629
+ *
1630
+ * An explicit `false` is a claim about the guarantee that was obtained, not a
1631
+ * detail: the pipeline reads it and reports `constraints.backend: "none"`, so
1632
+ * an adapter that had to ask for JSON in the prompt (for instance because the
1633
+ * model rejects `response_format`) must set it. Leaving it undefined asserts
1634
+ * nothing and keeps the planned backend.
1635
+ */
1376
1636
  structuredNatively?: boolean;
1377
1637
  /**
1378
1638
  * Por qué paró de generar el modelo. `"length"` significa que se quedó sin
@@ -1436,6 +1696,13 @@ declare function createMockProvider(options?: MockProviderOptions): Provider;
1436
1696
  * with a JSON schema when a contract is present, which is the provider-native
1437
1697
  * constrained-decoding path preferred by the layer.
1438
1698
  *
1699
+ * Models that predate structured outputs reject `response_format` with a 400. The
1700
+ * adapter degrades for those: it repeats the request once with the schema carried
1701
+ * in the prompt instead, remembers the model so later requests skip the rejected
1702
+ * attempt, and reports `structuredNatively: false` so the savings report states
1703
+ * `constraints.backend: "none"` rather than claiming a guarantee the provider
1704
+ * never gave.
1705
+ *
1439
1706
  * Network calls use global `fetch` (Node >= 18) so there is no SDK dependency.
1440
1707
  */
1441
1708
  interface OpenAICompatibleOptions {
@@ -1475,8 +1742,74 @@ declare class OpenAICompatibleProvider implements Provider {
1475
1742
  readonly id: string;
1476
1743
  private readonly opts;
1477
1744
  private readonly fetchImpl;
1745
+ /** Models of this endpoint already known to reject `response_format`. */
1746
+ private readonly sinResponseFormat;
1478
1747
  constructor(opts: OpenAICompatibleOptions);
1479
1748
  generate(req: ProviderRequest): Promise<ProviderResponse>;
1749
+ /** Build the request body and post it. `nativo` selects the `response_format` path. */
1750
+ private enviar;
1751
+ /** Translate a 2xx endpoint response into the provider-neutral shape. */
1752
+ private leer;
1753
+ }
1754
+
1755
+ /**
1756
+ * Anthropic Messages API adapter.
1757
+ *
1758
+ * Speaks `POST /v1/messages`: the system prompt travels in its own top-level
1759
+ * `system` field (not as a message), tools are `{name, description,
1760
+ * input_schema}` with no `function` wrapper, and prefix caching is requested
1761
+ * with EXPLICIT `cache_control` breakpoints — Anthropic does not cache on its
1762
+ * own. A cached read is billed at 0.1x the input rate, which is the largest
1763
+ * single lever on the input side of a customer's bill.
1764
+ *
1765
+ * Network calls use global `fetch` (Node >= 18) so there is no SDK dependency.
1766
+ */
1767
+ /** Render order of the request, which is also the order the cache prefix grows in. */
1768
+ interface AnthropicOptions {
1769
+ apiKey?: string;
1770
+ /** Defaults to the public API. Override for a gateway or a proxy. */
1771
+ baseUrl?: string;
1772
+ /** Wire version pinned in the `anthropic-version` header. */
1773
+ apiVersion?: string;
1774
+ /** Extra headers (e.g. for a gateway). */
1775
+ headers?: Record<string, string>;
1776
+ /** Injectable fetch for testing. */
1777
+ fetchImpl?: typeof fetch;
1778
+ /**
1779
+ * `max_tokens` is REQUIRED by this API — unlike the OpenAI shape, where it is
1780
+ * optional. A request without one is a 400, so the adapter has to supply a
1781
+ * value. It is deliberately generous: a low default would truncate answers
1782
+ * and the truncation would look like a model failure rather than an adapter
1783
+ * default.
1784
+ */
1785
+ defaultMaxOutputTokens?: number;
1786
+ /**
1787
+ * When true (recommended for measurement), a response whose `usage` block is
1788
+ * missing its counters is treated as a HARD ERROR instead of being coerced to
1789
+ * 0 tokens. An absent usage payload otherwise becomes `actual cost = 0` → a
1790
+ * fabricated 100% saving.
1791
+ */
1792
+ requireUsage?: boolean;
1793
+ /**
1794
+ * How many `cache_control` breakpoints the adapter may spend.
1795
+ *
1796
+ * The API caps them at 4 per request. The adapter never needs more than 2
1797
+ * (end of system, end of the stable message prefix) and the cap is exposed so
1798
+ * a gateway with a stricter limit can lower it. Raising it above 4 is
1799
+ * pointless: the extra breakpoints are rejected by the API, not ignored.
1800
+ */
1801
+ maxCacheBreakpoints?: number;
1802
+ }
1803
+ declare class AnthropicProvider implements Provider {
1804
+ readonly id: string;
1805
+ private readonly opts;
1806
+ private readonly fetchImpl;
1807
+ constructor(opts?: AnthropicOptions);
1808
+ generate(req: ProviderRequest): Promise<ProviderResponse>;
1809
+ private construirCuerpo;
1810
+ private marcarCache;
1811
+ /** Traduce una respuesta 2xx a la forma neutra del layer. */
1812
+ private leer;
1480
1813
  }
1481
1814
 
1482
1815
  /**
@@ -1874,6 +2207,13 @@ interface UsageEmitterOptions {
1874
2207
  * exactamente lo mismo que se ve cuando no hay tráfico—.
1875
2208
  */
1876
2209
  onAuthError?: (status: number) => void;
2210
+ /**
2211
+ * Silence the console warnings. They are ON by default, exactly as in the
2212
+ * revocation poller: metering is best-effort, so a broken ingest breaks
2213
+ * nothing visible — usage simply stops arriving, silently, and the customer's
2214
+ * dashboard stays empty forever.
2215
+ */
2216
+ silenceWarnings?: boolean;
1877
2217
  }
1878
2218
  /**
1879
2219
  * Buffers counts-only usage and flushes it to the control plane. DETECTIVE
@@ -1893,6 +2233,13 @@ declare class UsageEmitter {
1893
2233
  private ci;
1894
2234
  private timer;
1895
2235
  private flushing;
2236
+ /**
2237
+ * CONSECUTIVE failed flushes. Warn on the third and then every twelfth: one
2238
+ * lost batch is normal, a whole hour is not. Same cadence as the revocation
2239
+ * poller on purpose — two different rhythms for the same class of failure
2240
+ * only confuse whoever reads the logs.
2241
+ */
2242
+ private fallosSeguidos;
1896
2243
  constructor(options: UsageEmitterOptions);
1897
2244
  /** Set the resolved seat identity (derivation is async; records emitted before
1898
2245
  * it resolves are stamped at flush time, so none are lost). */
@@ -1908,6 +2255,19 @@ declare class UsageEmitter {
1908
2255
  * since retrying a rejected payload would loop forever.
1909
2256
  */
1910
2257
  flush(): Promise<void>;
2258
+ /**
2259
+ * THE INGEST HAS BEEN FAILING FOR A WHILE AND NOBODY NOTICED.
2260
+ *
2261
+ * Metering is best-effort by design — it must never take down the customer's
2262
+ * request — and that turns every failure into SILENT data loss. Without this,
2263
+ * the only symptom is an empty dashboard, which is indistinguishable from
2264
+ * "you haven't sent anything yet": the symptom points the opposite way from
2265
+ * the cause.
2266
+ *
2267
+ * Warns on the third consecutive failure and then every twelfth. Not the
2268
+ * first — a lost request is normal — and not every one, which becomes noise.
2269
+ */
2270
+ private avisarDeFallo;
1911
2271
  private requeue;
1912
2272
  private toWire;
1913
2273
  }
@@ -1953,6 +2313,17 @@ interface SavingsLayerConfig {
1953
2313
  schemaRegistry?: SchemaRegistry;
1954
2314
  constraintBackends?: ConstraintBackend[];
1955
2315
  compressor?: Compressor;
2316
+ /**
2317
+ * The numbers behind "compress only when it pays for itself" — in money, not
2318
+ * in tokens.
2319
+ *
2320
+ * All optional, and the default never invents a saving: the layer compresses
2321
+ * when the net token saving justifies it, and holds back when compressing
2322
+ * would push the prompt below the provider's cacheable minimum — where fewer
2323
+ * tokens can mean a larger bill. Declare {@link CompressionEconomics.prefix}
2324
+ * with your own measurements to unlock the full decision in both directions.
2325
+ */
2326
+ compressionEconomics?: CompressionEconomics;
1956
2327
  retriever?: Retriever;
1957
2328
  reranker?: Reranker;
1958
2329
  /**
@@ -2106,6 +2477,16 @@ interface SavingsLayerConfig {
2106
2477
  * in tests/benchmarks that want internal errors to surface.
2107
2478
  */
2108
2479
  failOpen?: boolean;
2480
+ /**
2481
+ * Silence BV-SALA's console warnings. They are ON by default, and they only
2482
+ * ever report a condition whose symptom is otherwise invisible: a saving knob
2483
+ * asked for but not wired up, usage that stops reaching the control plane, a
2484
+ * revocation list going stale. Nothing here throws and nothing here changes
2485
+ * what the layer does — turning it on only removes the console noise, so set
2486
+ * it when you have your own observability. Applies to the pipeline, the usage
2487
+ * emitter and the revocation poller alike.
2488
+ */
2489
+ silenceWarnings?: boolean;
2109
2490
  /** Injectable clock for deterministic tests. */
2110
2491
  now?: () => number;
2111
2492
  /** Injectable id generator for deterministic tests. */
@@ -2147,7 +2528,7 @@ interface SavingsLayer {
2147
2528
  * Create a savings layer. The only hard requirement is a `provider`; a default
2148
2529
  * heuristic tokenizer and in-memory implementations of every other component
2149
2530
  * are wired automatically. Replace any of them for production (Redis exact
2150
- * cache, pgvector/Qdrant semantic cache, a real tokenizer, an LLMLingua sidecar,
2531
+ * cache, a vector-backed semantic cache, a real tokenizer, a compressor sidecar,
2151
2532
  * XGrammar/llguidance backends, etc.).
2152
2533
  *
2153
2534
  * @example
@@ -2185,6 +2566,7 @@ declare function createSavingsLayer(config: Omit<SavingsLayerConfig, "tokenizer"
2185
2566
  * on the customer's machine. `test/version-declarada.test.ts` ties this
2186
2567
  * constant to package.json so the two can never drift.
2187
2568
  */
2188
- declare const BV_SALA_VERSION = "0.2.2";
2189
2569
 
2190
- export { BV_SALA_VERSION, type BivelioHandoff, type CacheOutcome, type CachedAnswer, type CanonicalRequest, type ChatMessage, type CompactVerification, 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, type Policy, type ProtectedSpan, type Provider, type ProviderRequest, type ProviderResponse, type ProviderUsage, 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, TOON_INSTRUCTION, type TokenEstimator, TokenizerRegistry, type ToolCallRequest, type ToolDescriptor, type ToolRisk, type ToolSelection, ToolSelector, type ToolSelectorOptions, type ToonDecodeOptions, type ToonOptions, TraceCollector, type TraceEvent, UsageEmitter, type UsageEmitterOptions, type ValidationError, type ValidationResult, adaptiveK, arithmeticResolver, buildExactKey, buildInflightKey, canonicalize, computeNextDelay, cosine, createDefaultResolvers, createHandoff, createMockProvider, createSavingsLayer, createToonSerializer, csvSerializer, decodeToon, dedupe, defaultTokenizerRegistry, deriveSeatId, deriveStatus, detectCi, emptyState, encodeToon, evalArithmetic, evaluatePolicy, executeRetrievalPlan, gateFromKey, heuristicEstimator, isKeyset, isStrictCompatible, jsonCompactSerializer, keyTokensMatch, liveGateFromKey, mergeDelta, noopCompressor, normalizeText, projectContext, providerNativeBackend, randomId, registerExactTokenizer, registrarTokenizadorExacto, renderState, resolveConstraintBackend, resolveDependencies, resolveLicense, sha256, shouldCompress, stableStringify, toonSerializer, uniformColumns, validateAgainstSchema, verifyCompactSignature, verifyLicenseToken, verifyRevocationList, withProtectedSpans };
2570
+ declare const BV_SALA_VERSION = "0.3.0";
2571
+
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 };