@bivelio/savings-layer 0.4.24 → 0.4.25

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
@@ -123,6 +123,8 @@ declare class SavingsLedger {
123
123
  cacheWriteInputTokens: number;
124
124
  /** The premium on those writes, already priced in USD. Adds to `actual` only. */
125
125
  cacheWritePremiumUsd: number;
126
+ /** Provider-initiated cache-write premium, priced in USD. Adds to baseline and `actual` alike. */
127
+ providerCacheWritePremiumUsd: number;
126
128
  /** Cuenta LOCAL del prompt que de verdad se envió al proveedor. */
127
129
  sentInputLocal: number;
128
130
  /** Tokens que la capa quitó de ese prompt, en cuenta LOCAL. */
@@ -191,11 +193,15 @@ declare class SavingsLedger {
191
193
  *
192
194
  * The caller raises `actualLlm`, because the call happened and the report owes
193
195
  * the reader that count.
196
+ *
197
+ * @param providerWritePremium the multiplier the provider bills on its own
198
+ * for the call's cache writes (`usage.cacheWriteInputTokens`); 1 = none.
194
199
  */
195
200
  cascadeAttempt(usage: {
196
201
  inputTokens: number;
197
202
  outputTokens: number;
198
- }, cheapPrice: CostModel): void;
203
+ cacheWriteInputTokens?: number;
204
+ }, cheapPrice: CostModel, providerWritePremium?: number): void;
199
205
  /** Record the premium for `tokens` of cache write billed at `multiplicador`x. */
200
206
  cacheWritePremium(tokens: number, multiplicador: number, precioDelQueMarco: CostModel): void;
201
207
  /**
@@ -216,6 +222,8 @@ declare class SavingsLedger {
216
222
  compress(originalTokens: number, compressedTokens: number): void;
217
223
  /** Ledger 4: tokens still present but billed/processed as provider-cached. */
218
224
  providerCached(tokens: number): void;
225
+ /** Record the premium for `tokens` of cache write the provider bills at `multiplier`x on its own. */
226
+ providerCacheWritePremium(tokens: number, multiplier: number, price: CostModel): void;
219
227
  /** Set when an avoided call is valued against the provider counter of an identical call that ran. */
220
228
  reference: ReferenceCredit | null;
221
229
  /** Set when an avoided cache or coalesced hit could not be valued by reference: why it stays estimated. */
@@ -264,20 +272,28 @@ declare class SavingsLedger {
264
272
  * catalogue stays out of the way.
265
273
  */
266
274
  /** When these prices were last checked against the providers' public rates. */
267
- declare const PRECIOS_AL_DIA = "2026-09-25";
275
+ declare const PRECIOS_AL_DIA = "2026-09-27";
268
276
  /**
269
277
  * Key = the model id as the provider publishes it, without a vendor prefix.
270
278
  * Lookup tries the exact id, then the id without its provider prefix, then the
271
279
  * LONGEST matching prefix — so `gpt-4o-2024-08-06` inherits `gpt-4o` without
272
280
  * having to enumerate every dated snapshot.
281
+ *
282
+ * These are the rows in force on {@link PRECIOS_AL_DIA}. A price change a
283
+ * provider has already announced is applied by {@link precioDeLista} from its
284
+ * effective date, so a request is always priced at the rate of its own day.
273
285
  */
274
286
  declare const CATALOGO: Readonly<Record<string, CostModel>>;
275
287
  /**
276
- * The list price for a model, or `undefined` when it is not in the catalogue.
288
+ * The list price for a model on the day of the request, or `undefined` when
289
+ * the catalogue has no price for it in force that day.
277
290
  *
278
291
  * @param model the id as the caller passes it, with or without a vendor prefix.
292
+ * @param at when the request was made (epoch ms or a `Date`); defaults to now.
293
+ * Prices resolve per UTC day, so a price change the provider has announced
294
+ * applies from its effective date.
279
295
  */
280
- declare function precioDeLista(model: string): CostModel | undefined;
296
+ declare function precioDeLista(model: string, at?: Date | number): CostModel | undefined;
281
297
 
282
298
  /**
283
299
  * The version the SDK reports for itself.
@@ -287,7 +303,7 @@ declare function precioDeLista(model: string): CostModel | undefined;
287
303
  * on the customer's machine. `test/version-declarada.test.ts` ties this
288
304
  * constant to package.json so the two can never drift.
289
305
  */
290
- declare const BV_SALA_VERSION = "0.4.24";
306
+ declare const BV_SALA_VERSION = "0.4.25";
291
307
 
292
308
  /**
293
309
  * Environment-agnostic UUID generation.
@@ -503,7 +519,28 @@ interface RetrievalOptions {
503
519
  }
504
520
  interface SavingsToggles {
505
521
  exactCache?: boolean;
506
- semanticCache?: boolean | "safe-only";
522
+ /**
523
+ * OPT-IN. Reuse the answer to an earlier, reworded version of this request.
524
+ * Needs a semantic cache on the layer (`createSavingsLayer({ semanticCache })`)
525
+ * and a license that includes it.
526
+ *
527
+ * - `true` and `"safe-only"` are the same setting: single-shot requests only.
528
+ * - `"include-agentic"` also admits agentic requests: those that declare tools
529
+ * (read-only ones included), carry tool calls or tool results, or hold more
530
+ * than one user turn.
531
+ *
532
+ * WHY AGENTIC TRAFFIC IS LEFT OUT BY DEFAULT. Two runs of an agent loop can
533
+ * differ only in what a tool returned. The deterministic guard under the
534
+ * similarity score compares figures and dates, negation and word overlap; it
535
+ * cannot tell that a tool said something else in words. Measure it on your own
536
+ * traffic before you include it.
537
+ *
538
+ * Whatever you set, requests with write or destructive tools, freshness-sensitive
539
+ * requests (including any freshness bucket other than `any`), personalized
540
+ * requests and stochastic sampling never use this cache. A hit is reported as
541
+ * `cache.result: "semantic"` and `qualityStatus: "task_equivalent"`.
542
+ */
543
+ semanticCache?: boolean | "safe-only" | "include-agentic";
507
544
  compression?: boolean | "auto";
508
545
  /**
509
546
  * OPT-IN. Elide old, large tool_results from a multi-turn context before the
@@ -655,6 +692,16 @@ interface SavingsToggles {
655
692
  * tier the request is sent unchanged and the report says so.
656
693
  */
657
694
  deferrable?: boolean;
695
+ /**
696
+ * Declare this request STANDALONE: nothing after its stable prefix will be sent again as
697
+ * the start of a later request (it is not a conversation turn you will continue).
698
+ * Defaults to false. Only acts on OpenAI models that bill prompt-cache writes above the
699
+ * input rate, on OpenAI's own API: the provider may then write its cache only at the
700
+ * stable prefix, or nowhere, instead of at the end of your message. Trade-off: repeats
701
+ * and continuations of this exact prompt lose the cache reads the provider default would
702
+ * give them. See report.cacheWrites. Never changes the answer.
703
+ */
704
+ standalone?: boolean;
658
705
  /**
659
706
  * Set to `false` to keep this request out of verification sampling: it is
660
707
  * counted as excluded and never sent twice. No effect when verification is off.
@@ -694,32 +741,33 @@ interface DeferredExecutionReport {
694
741
  */
695
742
  credited: boolean;
696
743
  /**
697
- * Fraction of the standard list price the served tier bills — `0.5` for a tier
698
- * documented at half price. Present only when {@link credited}.
744
+ * The effective fraction of the standard rate card this call was billed at:
745
+ * `tierSaving.appliedCost / tierSaving.standardCost` on the one call whose tier
746
+ * was read. `0.5` when every component of the call bills at half price; higher
747
+ * when some component (typically cached input) is discounted less or not at
748
+ * all. Present only when {@link credited}.
699
749
  */
700
750
  discountRate?: number;
751
+ /** @deprecated Use `tierSaving.amount`; same value. Kept for compatibility. */
752
+ estimatedTierSaving?: number;
701
753
  /**
702
- * What the tier was worth on this request, in your currency unit.
703
- *
704
- * A COUNTERFACTUAL priced from the standard rate card: this request was not
705
- * also run at standard price, so it is `(1 - discountRate)` applied to the
706
- * standard-rate cost of THE ONE CALL whose {@link servedTier} was read.
707
- *
708
- * ⚠️ IT IS NOT `cost.actual * (1 - discountRate)`, and the difference is not
709
- * rounding. `cost.actual` is the whole request: it can also carry a repair
710
- * retry, a tool-discovery probe, or a model-cascade attempt that was tried and
711
- * thrown away. No tier was read back for any of those, so none of them is
712
- * credited here even though they very likely got the discount too. Compute the
713
- * product yourself and you will get a LARGER number than this field — this
714
- * field is the part that was actually observed.
754
+ * What the served tier was worth on the one call whose tier was read: that call's
755
+ * provider-reported usage at the standard rate card minus the same usage at the rate
756
+ * card of the tier that served it, component by component. Present only when credited.
715
757
  *
716
- * ⚠️ `cost.actual` prices this request at the STANDARD rate card, so when this
717
- * field is present your invoice line will be LOWER than `cost.actual` by at
718
- * least this amount. The bias is deliberate and it is the safe one: the layer
719
- * under-reports the saving rather than promising one it cannot prove. The
720
- * reason it is not folded in is {@link creditedLedger}.
758
+ * It is NOT `cost.actual` times a discount. `cost.actual` is the whole request and
759
+ * can also carry a repair retry, a tool-discovery probe or a model-cascade attempt;
760
+ * no tier was read back for those, so none of them is credited here. And
761
+ * `cost.actual` prices the request at the STANDARD rate card, so when this is
762
+ * present your invoice line is lower than `cost.actual` by at least this amount.
721
763
  */
722
- estimatedTierSaving?: number;
764
+ tierSaving?: PriceLeverSaving;
765
+ /**
766
+ * True when the cheaper tier had no capacity and the request was repeated once at
767
+ * the standard tier (layer setting `priceLevers.onCapacityError: "standard"`).
768
+ * Nothing is credited then, and the refused attempt is not counted as a call.
769
+ */
770
+ capacityFallback?: boolean;
723
771
  /**
724
772
  * Which of the four ledgers this credits: none, and that is the honest answer.
725
773
  * Deferring avoids no call (ledger 1), removes no tokens (2), re-encodes
@@ -742,6 +790,44 @@ interface DeferredExecutionReport {
742
790
  /** Why it did what it did, in one sentence. Never empty. */
743
791
  reason: string;
744
792
  }
793
+ /**
794
+ * What a provider price lever was worth on one call, in your currency unit.
795
+ *
796
+ * It never enters `cost.*` and is never billed: it is reported next to the four
797
+ * ledgers, with its own basis.
798
+ */
799
+ interface PriceLeverSaving {
800
+ /** In your currency unit. Never more than the difference between the two published rate cards on this usage. */
801
+ amount: number;
802
+ /**
803
+ * "measured": the provider's own counter for this call at two published rate cards,
804
+ * the tier read back from the provider's official API, and the counter reporting how
805
+ * much of the input was read from (or, where billed apart, written to) the prompt
806
+ * cache. "estimated": anything short of that; `basisReason` says what.
807
+ */
808
+ basis: Extract<CostBasis, "measured" | "estimated">;
809
+ /** This call's provider-reported usage at the standard rate card. */
810
+ standardCost: number;
811
+ /** The same usage at the rate card that applied. */
812
+ appliedCost: number;
813
+ /** YYYY-MM-DD: when these rate cards were last checked against the provider's published prices. */
814
+ pricesCheckedOn: string;
815
+ /** One sentence. Never empty. */
816
+ basisReason: string;
817
+ }
818
+ /** Present whenever `savings.standalone` is true, including when it did nothing. */
819
+ interface CacheWriteReport {
820
+ mode: "explicit" | "provider-default";
821
+ /**
822
+ * Cache breakpoints the layer placed: 0 or 1. With 0 in explicit mode the provider
823
+ * neither reads nor writes its prefix cache.
824
+ */
825
+ breakpoints: number;
826
+ /** Always basis "estimated": the provider default was not run for this request. */
827
+ avoidedWritePremium?: PriceLeverSaving;
828
+ /** Why, in one sentence, including what it costs you. Never empty. */
829
+ reason: string;
830
+ }
745
831
  /**
746
832
  * How much authority the model cascade has over your requests.
747
833
  *
@@ -1049,6 +1135,155 @@ type CacheOutcome = "miss" | "exact" | "semantic" | "singleflight" | "determinis
1049
1135
  */
1050
1136
  declare const QUALITY_STATUSES: readonly ["exact", "task_equivalent", "not_verified", "bounded_degradation", "rejected"];
1051
1137
  type QualityStatus = (typeof QUALITY_STATUSES)[number];
1138
+ /** Where the answer you received came from on this request. */
1139
+ type AnswerSource =
1140
+ /** Your provider generated it for this request. */
1141
+ "provider"
1142
+ /** A replay of the answer already produced for this very request. */
1143
+ | "exact_cache"
1144
+ /** A reuse of the answer to a reworded version of this request. */
1145
+ | "semantic_cache"
1146
+ /** Shared with an identical request that was running at the same time. */
1147
+ | "coalesced"
1148
+ /** Computed locally; no model was involved. */
1149
+ | "deterministic"
1150
+ /** A direct provider call after an internal error; nothing was optimized. */
1151
+ | "fail_open";
1152
+ /** The output-contract check on the answer that was served. */
1153
+ interface QualitySchemaCheck {
1154
+ /**
1155
+ * `passed` or `failed`: the served answer was validated against
1156
+ * `response.schema`. `not_applicable`: the model asked to run tools, so there
1157
+ * is no final answer yet.
1158
+ */
1159
+ outcome: "passed" | "failed" | "not_applicable";
1160
+ /**
1161
+ * Whether the layer relied on a decode-time constraint for this answer (the
1162
+ * backend named in `constraints.backend`). The answer is validated either way.
1163
+ * Always false on cache and resolver answers. A coalesced answer carries the
1164
+ * value of the request it joined, which is the one that generated it.
1165
+ */
1166
+ decodeTimeConstraint: boolean;
1167
+ /** Validation errors in the served answer. 0 when it passed. */
1168
+ errorCount: number;
1169
+ /**
1170
+ * The model wrapped valid JSON in prose or a code fence, and it was extracted
1171
+ * locally instead of paying for a repair call.
1172
+ */
1173
+ unwrapped?: true;
1174
+ }
1175
+ /** What happened when the first answer failed its output contract. */
1176
+ interface QualityRepairCheck {
1177
+ /**
1178
+ * `repaired`: one repair call was made and its answer passed. `still_invalid`:
1179
+ * one repair call was made and its answer failed too. `skipped`: no repair
1180
+ * call was made; `skipReason` says why.
1181
+ */
1182
+ outcome: "repaired" | "still_invalid" | "skipped";
1183
+ /**
1184
+ * - `output_truncated`: the answer hit the output limit, and the same limit
1185
+ * would cut the repair off again.
1186
+ * - `schema_defect`: no answer can satisfy the schema (for example, a pattern
1187
+ * that is not a valid regular expression).
1188
+ * - `would_cost_more_than_no_layer`: repairing would have cost more than
1189
+ * sending the request without the layer (opt-in, `skipRepairAboveBaseline`).
1190
+ * - `decode_time_constraint`: generation was already constrained, so a retry
1191
+ * would not change the outcome.
1192
+ */
1193
+ skipReason?: "output_truncated" | "schema_defect" | "would_cost_more_than_no_layer" | "decode_time_constraint";
1194
+ }
1195
+ /** Whether the provider stopped because it reached the output limit. */
1196
+ interface QualityTruncationCheck {
1197
+ /** True when the provider reported `finishReason: "length"`. A truncated answer is never cached. */
1198
+ truncated: boolean;
1199
+ /** The stop reason exactly as the provider named it; absent when it reported none. */
1200
+ finishReason?: string;
1201
+ /**
1202
+ * Whether you set the limit (`response.maxOutputTokens`). When false, any
1203
+ * limit that applied was a provider or adapter default.
1204
+ */
1205
+ capSetByCaller: boolean;
1206
+ /** The limit you set, in output tokens. Present only when `capSetByCaller`. */
1207
+ outputLimit?: number;
1208
+ }
1209
+ /** How a cache hit matched this request. */
1210
+ type QualityCacheMatch = {
1211
+ kind: "exact";
1212
+ /** Age of the replayed answer, in milliseconds. */
1213
+ ageMs?: number;
1214
+ } | {
1215
+ kind: "semantic";
1216
+ /** Age of the reused answer, in milliseconds. */
1217
+ ageMs?: number;
1218
+ /**
1219
+ * Similarity between this request and the stored one. It is not evidence
1220
+ * of equivalence: see {@link QualityStatus}.
1221
+ */
1222
+ similarity: number;
1223
+ /** The similarity a candidate had to reach to be considered at all. */
1224
+ similarityFloor: number;
1225
+ /**
1226
+ * The confidence the hit had to reach to be served. Confidence is the
1227
+ * lower of `similarity` and `verifierScore` (`cache.confidence`).
1228
+ */
1229
+ acceptFloor: number;
1230
+ /** Your verifier's score, present only when you configured one. */
1231
+ verifierScore?: number;
1232
+ /** `built_in`: the lexical embedder shipped with the SDK. `custom`: yours. */
1233
+ embedder: "built_in" | "custom";
1234
+ /**
1235
+ * The deterministic guard passed, turn by turn: figures and dates,
1236
+ * negation parity and content-word overlap. A hit is never served without it.
1237
+ */
1238
+ factGuard: "passed";
1239
+ };
1240
+ /** What the model cascade decided on this request. See {@link ModelCascadeReport}. */
1241
+ interface QualityCascadeCheck {
1242
+ mode: "shadow" | "on";
1243
+ /** What the deterministic gate was able to check (`modelCascade.verifier`). */
1244
+ gate: "none" | "structural" | "value-constrained";
1245
+ /**
1246
+ * - `accepted`: a cheaper model answered and its answer cleared the gate.
1247
+ * - `escalated`: the gate rejected the cheaper answer and the model you asked for answered.
1248
+ * - `not_routed`: the cascade is on but this request was not routed (see `modelCascade.reason`).
1249
+ * - `not_applied`: shadow mode. The cascade only evaluated this request and
1250
+ * the model you asked for answered. On a sampled request
1251
+ * (`modelCascade.sampled`) the cheaper model also ran, and its answer was
1252
+ * not served.
1253
+ */
1254
+ outcome: "accepted" | "escalated" | "not_routed" | "not_applied";
1255
+ /**
1256
+ * Shadow sampling only: both answers satisfied the contract and disagreed on
1257
+ * a hard value. A hint, never proof.
1258
+ */
1259
+ sampleDiverged?: boolean;
1260
+ }
1261
+ /**
1262
+ * What the layer checked on the answer it served, and what came of it.
1263
+ *
1264
+ * It holds no money and no token counts. It is never added to the four savings
1265
+ * ledgers or to `cost`, and it never leaves your process: usage reporting does
1266
+ * not carry it. `qualityStatus` is the one-word verdict; this is the evidence
1267
+ * behind it. A block is present only when its check applies to this request.
1268
+ * With `source: "coalesced"`, the blocks describe the shared answer as the
1269
+ * request it joined produced it: nothing was run again. An absent ledger means
1270
+ * unknown (an older SDK, or a report built elsewhere).
1271
+ */
1272
+ interface QualityLedger {
1273
+ source: AnswerSource;
1274
+ /** Only with `source: "coalesced"`: where the shared answer came from for the request it joined. */
1275
+ sharedFrom?: Exclude<AnswerSource, "coalesced">;
1276
+ /** Present when the request carried `response.schema`. */
1277
+ schema?: QualitySchemaCheck;
1278
+ /** Present when a provider answer on this request failed its first validation. */
1279
+ repair?: QualityRepairCheck;
1280
+ /** Present when a provider generated the served answer. */
1281
+ truncation?: QualityTruncationCheck;
1282
+ /** Present when the answer came from the exact or the semantic cache. */
1283
+ cacheMatch?: QualityCacheMatch;
1284
+ /** Present when `savings.modelCascade` is not `"off"`. */
1285
+ cascade?: QualityCascadeCheck;
1286
+ }
1052
1287
  interface SavingsReport {
1053
1288
  requestId: string;
1054
1289
  /**
@@ -1148,6 +1383,12 @@ interface SavingsReport {
1148
1383
  * the `exact` label.
1149
1384
  */
1150
1385
  qualityStatus?: QualityStatus;
1386
+ /**
1387
+ * The evidence behind `qualityStatus`: where the answer came from and which
1388
+ * checks it went through, with no money in it. See {@link QualityLedger}.
1389
+ * Optional and additive: absent means unknown.
1390
+ */
1391
+ qualityLedger?: QualityLedger;
1151
1392
  /**
1152
1393
  * What the model cascade did, or would have done. Present whenever
1153
1394
  * `savings.modelCascade` is not `"off"`. Read its docstring before turning the
@@ -1159,6 +1400,11 @@ interface SavingsReport {
1159
1400
  * `savings.deferrable` is `true`, including when it got you nothing.
1160
1401
  */
1161
1402
  deferred?: DeferredExecutionReport;
1403
+ /**
1404
+ * Where the provider was allowed to write its prompt cache. Present whenever
1405
+ * `savings.standalone` is `true`, including when it changed nothing.
1406
+ */
1407
+ cacheWrites?: CacheWriteReport;
1162
1408
  /** How a streamed request was delivered. Present only on results of `generateStream`. */
1163
1409
  streaming?: StreamingReport;
1164
1410
  /**
@@ -1406,64 +1652,50 @@ declare class TokenizerRegistry {
1406
1652
  declare const defaultTokenizerRegistry: TokenizerRegistry;
1407
1653
 
1408
1654
  /**
1409
- * Tokenizador EXACTO — la regla del proveedor, no una aproximación.
1655
+ * Exact cache.
1410
1656
  *
1411
- * Por qué existe. El baseline de una llamada que se ejecutó se reconstruye como
1412
- * "lo enviado + lo quitado". Lo enviado lo mide el proveedor; lo quitado lo
1413
- * contamos aquí. Con el estimador heurístico esos dos sumandos vienen de reglas
1414
- * distintas, y la diferencia NO es ruido: el heurístico cobra cada símbolo suelto
1415
- * como un token entero, así que infla el JSON (40% de símbolos) mucho más que el
1416
- * An estimator that is not calibrated for the destination model can miss by
1417
- * enough to publish a baseline several times the real one, which would credit a
1418
- * saving that never existed. That is why `basis` degrades instead of claiming.
1657
+ * When the canonical key represents the request completely, a hit removes 100%
1658
+ * of input tokens, 100% of output tokens, and the whole provider call. This is
1659
+ * the single highest-value, lowest-risk saving in the layer. The interface is
1660
+ * deliberately minimal (get/set with TTL) so any store — Redis, a KV, an LRU —
1661
+ * can back it.
1419
1662
  */
1420
-
1421
- /** Lo que necesitamos de gpt-tokenizer. Tipado aquí para no depender de sus tipos. */
1422
- interface BpeEncoder {
1423
- encode(text: string): number[] | {
1424
- length: number;
1425
- };
1663
+ interface CachedAnswer<T = unknown> {
1664
+ text: string;
1665
+ data?: T;
1666
+ rendered?: string;
1667
+ /** Epoch ms when stored; used to compute age and honor TTL. */
1668
+ storedAtMs: number;
1669
+ /**
1670
+ * Optional metadata carried alongside the answer.
1671
+ *
1672
+ * `meta.reference` carries the provider counter of the call that produced this
1673
+ * answer, when that call ran untouched; readers that do not know it ignore it.
1674
+ */
1675
+ meta?: Record<string, unknown>;
1426
1676
  }
1427
- /**
1428
- * Estimador exacto sobre una codificación BPE ya cargada.
1429
- *
1430
- * Se construye con el encoder ya resuelto en vez de importarlo dentro: así
1431
- * este módulo no obliga a nadie a tener el paquete, y quien lo tiene decide qué
1432
- * codificación carga (o200k_base para gpt-4o/4.1/5, cl100k_base para 3.5/4).
1433
- */
1434
- declare class ExactTokenizer implements TokenEstimator {
1435
- private readonly encoder;
1436
- readonly id: string;
1437
- readonly exact = true;
1438
- constructor(id: string, encoder: BpeEncoder);
1439
- count(text: string): number;
1677
+ interface ExactCache {
1678
+ get<T = unknown>(key: string): Promise<CachedAnswer<T> | null>;
1679
+ set<T = unknown>(key: string, value: CachedAnswer<T>, ttlMs?: number): Promise<void>;
1680
+ delete(key: string): Promise<void>;
1440
1681
  }
1441
- /** Modelos que usan cada codificación. Sirve para mapear el registro de una vez. */
1442
- declare const MODELS_BY_ENCODING: Readonly<Record<string, readonly string[]>>;
1443
- /**
1444
- * Carga `gpt-tokenizer` si está instalado y registra el tokenizador exacto para
1445
- * los modelos de OpenAI. Devuelve `false` —sin lanzar— cuando no está: la
1446
- * ausencia del paquete opcional no es un error, solo significa que la
1447
- * contabilidad se publicará como estimada.
1448
- *
1449
- * Se importa solo la codificación pedida, no el paquete entero: `gpt-tokenizer`
1450
- * expone un punto de entrada por codificación y cargarlas todas serían decenas
1451
- * de MB en memoria sin motivo.
1452
- */
1453
- declare function registerExactTokenizer(registro: {
1454
- register(e: TokenEstimator): unknown;
1455
- mapModel(model: string, tokenizerId: string): unknown;
1456
- }, opciones?: {
1457
- codificaciones?: readonly string[];
1458
- }): Promise<boolean>;
1459
1682
  /**
1460
- * @deprecated Spanish-named aliases kept for one minor version. Use
1461
- * {@link MODELS_BY_ENCODING} and {@link registerExactTokenizer}. The public
1462
- * surface of this package is English-only; these leaked and are on the way out.
1683
+ * In-memory exact cache with TTL and a size bound (LRU-ish by insertion order).
1684
+ * Suitable for tests and single-process deployments; swap for Redis in prod.
1463
1685
  */
1464
- declare const MODELOS_POR_CODIFICACION: Readonly<Record<string, readonly string[]>>;
1465
- /** @deprecated Use {@link registerExactTokenizer}. */
1466
- declare const registrarTokenizadorExacto: typeof registerExactTokenizer;
1686
+ declare class InMemoryExactCache implements ExactCache {
1687
+ private readonly store;
1688
+ private readonly maxEntries;
1689
+ private readonly now;
1690
+ constructor(opts?: {
1691
+ maxEntries?: number;
1692
+ now?: () => number;
1693
+ });
1694
+ get<T = unknown>(key: string): Promise<CachedAnswer<T> | null>;
1695
+ set<T = unknown>(key: string, value: CachedAnswer<T>, ttlMs?: number): Promise<void>;
1696
+ delete(key: string): Promise<void>;
1697
+ get size(): number;
1698
+ }
1467
1699
 
1468
1700
  /**
1469
1701
  * Canonicalization turns a raw request into a normalized, hashable form. This
@@ -1530,209 +1762,25 @@ declare function modelFamilyOf(model: string): string;
1530
1762
  declare function canonicalize(request: SavingsRequest): CanonicalRequest;
1531
1763
 
1532
1764
  /**
1533
- * Exact-cache key.
1534
- *
1535
- * A hash of the user text alone is NOT a safe cache key. Two requests may share
1536
- * text but differ in tenant, permission scope, model version, system prompt,
1537
- * schema, toolset, locale, sampling, knowledge epoch or safety policy — any of
1538
- * which can change the correct answer. The exact key includes all of them.
1539
- *
1540
- * `servedModel` CARRIES THE MODEL THAT WILL ACTUALLY ANSWER, not the one the
1541
- * caller named, and that distinction is what stops the model cascade from
1542
- * poisoning the cache. Routing to a cheap model and storing its answer under the
1543
- * STRONG model's key would hand that answer to the next request — one with the
1544
- * cascade off, or from another process entirely — while the report claims the
1545
- * requested model served it. That is a silent model swap, deferred, which is the
1546
- * worst kind of silent model swap.
1547
- *
1548
- * `modelFamily` is derived from whichever id is used, for the same reason: it is
1549
- * part of the identity of whoever answers.
1550
- */
1551
- declare function buildExactKey(c: CanonicalRequest, opciones?: {
1552
- servedModel?: string;
1553
- }): string;
1554
- /**
1555
- * In-flight (singleflight) key. Coalesces concurrent, equivalent requests onto
1556
- * a single execution. It can be slightly looser than the exact key (it does not
1557
- * need freshness bucketing, since the work is happening *now*), but must still
1558
- * isolate tenants and permissions.
1559
- *
1560
- * `schema` is REQUIRED here, even though `contract` is already part of the key:
1561
- * two concurrent calls that differ only in `schemaVersion` are different
1562
- * requests and must never coalesce. All three doors — exact key, in-flight key
1563
- * and semantic filter — key on the same identity, by design.
1564
- *
1565
- * `servedModel` CARRIES THE MODEL THAT WILL ACTUALLY ANSWER, for the same reason
1566
- * {@link buildExactKey} does, and this door is the one that leaks FIRST. The
1567
- * exact key protects the answer AFTER it is stored; this one protects it while
1568
- * it is still being written, and a follower does not re-run anything — it is
1569
- * handed the leader's text verbatim.
1570
- *
1571
- * The leak it closes, measured: two concurrent requests for the same text, one
1572
- * with `savings.modelCascade: "on"` and one with the cascade OFF, coalesced onto
1573
- * one execution. The routed leader called the cheap model, and the caller who
1574
- * never opted in — whose own decision was "do not route" — was served the cheap
1575
- * model's answer. Same shape inside a single uniformly-configured deployment:
1576
- * the automatic shut-off can latch between two concurrent requests, so one is
1577
- * routed and the other is not, and coalescing hands one of them the other's
1578
- * model. Whoever the layer decided must go to the strong model has to actually
1579
- * go there.
1580
- *
1581
- * `deferrable` CLOSES THE SAME HOLE ON THE LATENCY AXIS, which is why it belongs
1582
- * here and NOT in {@link buildExactKey}. The two doors do not risk the same
1583
- * thing: the exact key hands out an answer that is ALREADY WRITTEN, where the
1584
- * processing tier only picked a queue and changes neither the text nor the wait
1585
- * of whoever receives it. This one binds a follower to a call that HAS NOT COME
1586
- * BACK YET, so it imposes the leader's wait on them.
1587
- *
1588
- * A caller who never set `savings.deferrable` must not be made to wait for one
1589
- * who did.
1590
- */
1591
- declare function buildInflightKey(c: CanonicalRequest, opciones?: {
1592
- servedModel?: string;
1593
- deferrable?: boolean;
1594
- toolDiscovery?: boolean;
1595
- maxOutputTokens?: number;
1596
- }): string;
1597
-
1598
- /**
1599
- * Policy engine: risk and freshness.
1765
+ * Semantic cache (safe-by-construction).
1600
1766
  *
1601
- * This is the gate that decides which savings are *safe* for a given request.
1602
- * The most dangerous failure of a savings layer is not a cache miss — it is
1603
- * confidently returning a wrong answer (a stale price, another tenant's data, a
1604
- * reused answer for a side-effecting action). The policy engine exists to make
1605
- * those outcomes structurally impossible, not merely unlikely.
1767
+ * A semantic cache reuses a previous answer for a *similar* query. Its danger is
1768
+ * the false positive: confidently returning a valid answer to a nearby-but-wrong
1769
+ * question. Therefore the headline metric here is NOT hit ratio — it is the
1770
+ * PRECISION of hits. Every candidate must pass: tenant/permission filter,
1771
+ * knowledge-epoch filter, a similarity threshold, a stronger local verifier, a
1772
+ * freshness check, and an output-contract compatibility check.
1606
1773
  */
1607
- interface Policy {
1608
- /** May the exact cache be read/written for this request? */
1609
- allowExactCache: boolean;
1610
- /** May the semantic cache even be *considered*? (still verified later) */
1611
- allowSemanticCache: boolean;
1612
- /** May concurrent equivalent requests be coalesced? */
1613
- allowSingleflight: boolean;
1614
- /** May we run natural-language compression on the context? */
1615
- allowCompression: boolean;
1616
- /**
1617
- * May the SDK elide old, large tool_results before resending? Computed, not
1618
- * hardcoded like `allowCompression`: eliding drops whole tool outputs, so it
1619
- * honors the same vetoes as the semantic cache — write tools, freshness,
1620
- * personalization, stochastic sampling and `day`.
1621
- */
1622
- allowElision: boolean;
1623
- /** Does the request demand current/live data? */
1624
- requiresFreshData: boolean;
1625
- /** Does the request have (or request) side effects? */
1626
- hasSideEffects: boolean;
1627
- /** Is the response personalized to a user/account? */
1628
- isPersonalized: boolean;
1774
+ interface EmbeddingFn {
1775
+ (text: string): Promise<number[]> | number[];
1776
+ }
1777
+ interface SemanticVerifier {
1629
1778
  /**
1630
- * Techo de antigüedad para una entrada de caché exacta, derivado del
1631
- * `freshnessBucket`. `null` = sin techo propio (manda el TTL por defecto).
1632
- * Existe porque el bucket NO particiona la clave —va crudo al hash y no rota—,
1633
- * así que sin esto "hour" y "day" no significaban nada operativamente.
1779
+ * Second-stage check (ideally a small cross-encoder). Returns a confidence in
1780
+ * [0,1] that the candidate answer genuinely answers the query. The pipeline
1781
+ * only accepts a hit above the configured acceptance threshold.
1634
1782
  */
1635
- maxCacheAgeMs: number | null;
1636
- reasons: string[];
1637
- }
1638
- declare function evaluatePolicy(c: CanonicalRequest): Policy;
1639
-
1640
- /**
1641
- * Exact cache.
1642
- *
1643
- * When the canonical key represents the request completely, a hit removes 100%
1644
- * of input tokens, 100% of output tokens, and the whole provider call. This is
1645
- * the single highest-value, lowest-risk saving in the layer. The interface is
1646
- * deliberately minimal (get/set with TTL) so any store — Redis, a KV, an LRU —
1647
- * can back it.
1648
- */
1649
- interface CachedAnswer<T = unknown> {
1650
- text: string;
1651
- data?: T;
1652
- rendered?: string;
1653
- /** Epoch ms when stored; used to compute age and honor TTL. */
1654
- storedAtMs: number;
1655
- /**
1656
- * Optional metadata carried alongside the answer.
1657
- *
1658
- * `meta.reference` carries the provider counter of the call that produced this
1659
- * answer, when that call ran untouched; readers that do not know it ignore it.
1660
- */
1661
- meta?: Record<string, unknown>;
1662
- }
1663
- interface ExactCache {
1664
- get<T = unknown>(key: string): Promise<CachedAnswer<T> | null>;
1665
- set<T = unknown>(key: string, value: CachedAnswer<T>, ttlMs?: number): Promise<void>;
1666
- delete(key: string): Promise<void>;
1667
- }
1668
- /**
1669
- * In-memory exact cache with TTL and a size bound (LRU-ish by insertion order).
1670
- * Suitable for tests and single-process deployments; swap for Redis in prod.
1671
- */
1672
- declare class InMemoryExactCache implements ExactCache {
1673
- private readonly store;
1674
- private readonly maxEntries;
1675
- private readonly now;
1676
- constructor(opts?: {
1677
- maxEntries?: number;
1678
- now?: () => number;
1679
- });
1680
- get<T = unknown>(key: string): Promise<CachedAnswer<T> | null>;
1681
- set<T = unknown>(key: string, value: CachedAnswer<T>, ttlMs?: number): Promise<void>;
1682
- delete(key: string): Promise<void>;
1683
- get size(): number;
1684
- }
1685
-
1686
- /**
1687
- * Singleflight (in-flight coalescing).
1688
- *
1689
- * When N equivalent requests arrive at once, exactly one executes the work and
1690
- * the other N-1 await the same promise. This is NOT a historical cache — it is
1691
- * de-duplication of concurrent work, so it carries no staleness risk and needs
1692
- * no TTL. On traffic spikes it can eliminate a large fraction of duplicate
1693
- * provider calls for free.
1694
- */
1695
- declare class Singleflight {
1696
- private readonly inflight;
1697
- /** How many callers are currently sharing an in-flight execution. */
1698
- private readonly waiters;
1699
- /**
1700
- * Run `fn` under `key`. If a call with the same key is already running, share
1701
- * its result instead of starting a new one. Returns the shared result plus
1702
- * whether this caller was a coalesced follower (a "hit").
1703
- */
1704
- run<T>(key: string, fn: () => Promise<T>): Promise<{
1705
- value: T;
1706
- coalesced: boolean;
1707
- }>;
1708
- /**
1709
- * The in-flight execution under `key`, if one is running, joined as a
1710
- * follower; `undefined` when nothing is in flight. Never starts one.
1711
- */
1712
- join<T>(key: string): Promise<T> | undefined;
1713
- get activeKeys(): number;
1714
- }
1715
-
1716
- /**
1717
- * Semantic cache (safe-by-construction).
1718
- *
1719
- * A semantic cache reuses a previous answer for a *similar* query. Its danger is
1720
- * the false positive: confidently returning a valid answer to a nearby-but-wrong
1721
- * question. Therefore the headline metric here is NOT hit ratio — it is the
1722
- * PRECISION of hits. Every candidate must pass: tenant/permission filter,
1723
- * knowledge-epoch filter, a similarity threshold, a stronger local verifier, a
1724
- * freshness check, and an output-contract compatibility check.
1725
- */
1726
- interface EmbeddingFn {
1727
- (text: string): Promise<number[]> | number[];
1728
- }
1729
- interface SemanticVerifier {
1730
- /**
1731
- * Second-stage check (ideally a small cross-encoder). Returns a confidence in
1732
- * [0,1] that the candidate answer genuinely answers the query. The pipeline
1733
- * only accepts a hit above the configured acceptance threshold.
1734
- */
1735
- (query: string, candidateQuery: string): Promise<number> | number;
1783
+ (query: string, candidateQuery: string): Promise<number> | number;
1736
1784
  }
1737
1785
  interface SemanticRecord {
1738
1786
  embedding: number[];
@@ -1753,10 +1801,14 @@ interface SemanticRecord {
1753
1801
  /**
1754
1802
  * The COMPLETE identity of the request that produced this answer.
1755
1803
  *
1756
- * Every field here is a hard filter, checked before similarity is scored, and
1757
- * the set is identical to the one the exact cache keys on. Two questions
1758
- * worded alike but carrying different attached data, a different model or a
1759
- * different system prompt are different questions, and never match.
1804
+ * Every field here is a hard filter, checked before similarity is scored.
1805
+ * Together with the isolation fields above they are the identity the exact
1806
+ * cache keys on, except for the text itself, which is what similarity
1807
+ * compares. Freshness is covered by construction: the semantic cache only
1808
+ * runs on requests whose freshness bucket is `any`, the default. Two questions worded
1809
+ * alike but carrying different attached data, a different model, a different
1810
+ * system prompt, a different tool catalog or different sampling controls are
1811
+ * different questions, and never match.
1760
1812
  */
1761
1813
  model: string;
1762
1814
  systemPromptHash: string;
@@ -1764,14 +1816,30 @@ interface SemanticRecord {
1764
1816
  locale: string;
1765
1817
  schemaVersion: string;
1766
1818
  safetyPolicyVersion: string;
1819
+ /** Hash of the declared tool catalog (ids, risk and parameters). */
1820
+ toolsetHash: string;
1821
+ /** Sampling temperature of the request, or null when it set none. */
1822
+ temperature: number | null;
1823
+ /** Sampling seed of the request, or null when it set none. */
1824
+ seed: number | null;
1767
1825
  storedAtMs: number;
1768
1826
  }
1769
1827
  interface SemanticHit {
1770
1828
  record: SemanticRecord;
1771
1829
  similarity: number;
1830
+ /** Your verifier's score, or the similarity itself when no verifier is configured (see `verifierRan`). */
1772
1831
  verifierScore: number;
1832
+ /** The lower of `similarity` and `verifierScore`; a hit is served only at or above `acceptFloor`. */
1773
1833
  confidence: number;
1774
1834
  ageMs: number;
1835
+ /** Similarity a candidate had to reach to be considered at all. */
1836
+ similarityFloor: number;
1837
+ /** Confidence a candidate had to reach to be served. */
1838
+ acceptFloor: number;
1839
+ /** `built_in`: the lexical embedder shipped with the SDK. `custom`: the one you passed as `embed`. */
1840
+ embedder: "built_in" | "custom";
1841
+ /** Whether a verifier scored this hit. When false, `verifierScore` equals `similarity`. */
1842
+ verifierRan: boolean;
1775
1843
  }
1776
1844
  interface SemanticCacheOptions {
1777
1845
  /**
@@ -1820,6 +1888,7 @@ declare class InMemorySemanticCache {
1820
1888
  private readonly ttlMs;
1821
1889
  private readonly maxRecords;
1822
1890
  private readonly now;
1891
+ private readonly embebedorPropio;
1823
1892
  constructor(opts?: SemanticCacheOptions);
1824
1893
  /**
1825
1894
  * Find a safe hit for a canonical request, or null. Metadata filters run
@@ -1834,61 +1903,34 @@ declare class InMemorySemanticCache {
1834
1903
  }
1835
1904
 
1836
1905
  /**
1837
- * A LOCAL EMBEDDING FUNCTION — no network, no API key, no per-call cost.
1838
- *
1839
- * The semantic cache needs a way to compare meaning. Wiring it to a hosted
1840
- * embedding endpoint is the obvious answer and a bad default: it makes every
1841
- * miss cost a network round-trip and money, and it forces a provider choice onto
1842
- * a customer who never asked for one. So the cache shipped with no embedder at
1843
- * all, which means it never matched anything.
1844
- *
1845
- * This is the third option: a deterministic bag-of-features vector computed in
1846
- * microseconds from the text itself. It captures LEXICAL closeness — the same
1847
- * question asked with different filler words — which is the overwhelming majority
1848
- * of real repeat traffic. It does NOT capture deep semantics: "what is a CDN"
1849
- * and "explain content delivery networks" share no tokens and will not match.
1850
- *
1851
- * WHY THAT IS SAFE, AND THIS IS THE PART THAT MATTERS. A semantic cache's one
1852
- * unforgivable failure is confidently answering a NEARBY BUT DIFFERENT question.
1853
- * This vector never gets the chance: the cache runs a deterministic precision
1854
- * guard beneath the embedding — matching numbers and dates, negation parity and a
1855
- * content-overlap floor — and every isolation field (tenant, principal, model,
1856
- * schema, attached data) is a hard filter applied before similarity is scored.
1857
- * A weak embedding can only cause a MISS, which costs one real call. It cannot
1858
- * produce a wrong answer.
1859
- *
1860
- * Pass your own `embed` to {@link InMemorySemanticCache} to get true semantics.
1861
- * This one exists so that the feature is worth something with zero setup.
1862
- */
1863
- /**
1864
- * The vector for a piece of text.
1865
- *
1866
- * Features are unigrams plus adjacent bigrams: bigrams are what keep word ORDER
1867
- * from being free. Without them "does A cause B" and "does B cause A" are the
1868
- * same point in space.
1869
- *
1870
- * Term weight is sublinear (`1 + log tf`) so a word repeated forty times does not
1871
- * dominate the direction of the vector, and the result is L2-normalised so cosine
1872
- * similarity is a plain dot product.
1873
- */
1874
- declare function vectorLocal(texto: string): number[];
1875
- /**
1876
- * The default embedding function for {@link InMemorySemanticCache}.
1877
- *
1878
- * Synchronous by design — it returns the array, not a promise — so a cache lookup
1879
- * adds no await and no latency to the request path.
1880
- */
1881
- declare function embebedorLocal(texto: string): number[];
1882
- /**
1883
- * The similarity floor this embedder is calibrated for.
1906
+ * Singleflight (in-flight coalescing).
1884
1907
  *
1885
- * A lexical vector sits lower than a sentence-transformer on genuine paraphrases,
1886
- * so the 0.9 default that suits a hosted embedder would reject almost everything
1887
- * here. This value was measured on paraphrase pairs against near-miss pairs, not
1888
- * guessed — see `test/embebedor-local.test.ts`, which fails if the two
1889
- * populations stop being separable.
1908
+ * When N equivalent requests arrive at once, exactly one executes the work and
1909
+ * the other N-1 await the same promise. This is NOT a historical cache — it is
1910
+ * de-duplication of concurrent work, so it carries no staleness risk and needs
1911
+ * no TTL. On traffic spikes it can eliminate a large fraction of duplicate
1912
+ * provider calls for free.
1890
1913
  */
1891
- declare const UMBRAL_LOCAL = 0.72;
1914
+ declare class Singleflight {
1915
+ private readonly inflight;
1916
+ /** How many callers are currently sharing an in-flight execution. */
1917
+ private readonly waiters;
1918
+ /**
1919
+ * Run `fn` under `key`. If a call with the same key is already running, share
1920
+ * its result instead of starting a new one. Returns the shared result plus
1921
+ * whether this caller was a coalesced follower (a "hit").
1922
+ */
1923
+ run<T>(key: string, fn: () => Promise<T>): Promise<{
1924
+ value: T;
1925
+ coalesced: boolean;
1926
+ }>;
1927
+ /**
1928
+ * The in-flight execution under `key`, if one is running, joined as a
1929
+ * follower; `undefined` when nothing is in flight. Never starts one.
1930
+ */
1931
+ join<T>(key: string): Promise<T> | undefined;
1932
+ get activeKeys(): number;
1933
+ }
1892
1934
 
1893
1935
  /**
1894
1936
  * Deterministic resolvers.
@@ -2060,192 +2102,33 @@ declare class ToolSelector {
2060
2102
  }
2061
2103
 
2062
2104
  /**
2063
- * Retrieval planning: retrieve -> dedupe -> rerank -> adaptive-k.
2064
- *
2065
- * Eliminating irrelevant context is cheaper and safer than compressing it, so
2066
- * retrieval prunes aggressively BEFORE anything reaches the prompt. Documents
2067
- * are kept out of the prompt where possible and passed as references/hashes;
2068
- * only the selected, trimmed evidence is projected in.
2105
+ * A serializer encodes a structured payload into prompt text, and reports the
2106
+ * instruction overhead a model needs to *understand* that encoding. The format
2107
+ * selector compares `tokens(encode) + tokens(instruction)` across serializers.
2069
2108
  */
2070
- interface EvidenceDoc {
2071
- id: string;
2072
- text: string;
2073
- /** Optional pre-computed relevance score from the retriever. */
2074
- score?: number;
2075
- source?: string;
2076
- /** Content hash, so large docs can be referenced instead of inlined. */
2077
- hash?: string;
2078
- }
2079
- interface Retriever {
2080
- retrieve(query: string, k: number): Promise<EvidenceDoc[]>;
2081
- }
2082
- interface Reranker {
2083
- rerank(query: string, docs: EvidenceDoc[]): Promise<EvidenceDoc[]>;
2084
- }
2085
- interface RetrievalPlan {
2086
- execution: ExecutionClass;
2087
- query: string;
2088
- maxRounds: number;
2089
- maxDocuments: number;
2090
- }
2091
- interface RetrievalOutcome {
2092
- docs: EvidenceDoc[];
2093
- rounds: number;
2094
- retrievedCount: number;
2095
- keptCount: number;
2109
+ interface Serializer {
2110
+ readonly format: SerializationFormat;
2111
+ /** Whether this serializer can represent the given value at all. */
2112
+ supports(value: unknown): boolean;
2113
+ encode(value: unknown): string;
2114
+ /**
2115
+ * The instruction text a model needs to interpret this format. For JSON this
2116
+ * is empty; for row/CSV it is the per-request cost that must be amortized
2117
+ * before the format is worthwhile.
2118
+ */
2119
+ instruction(value: unknown): string;
2120
+ /**
2121
+ * Decode encoded text back to a value, used ONLY for the selector's round-trip
2122
+ * fidelity check. A serializer without a faithful decoder is treated as
2123
+ * UNVERIFIABLE: the selector will not choose it (fail-closed), so we never ship
2124
+ * — or bill savings for — an encoding we can't prove reproduces the payload.
2125
+ * JSON-compact is inherently lossless and is exempt from the check.
2126
+ */
2127
+ decode?(text: string): unknown;
2096
2128
  }
2097
- /** Remove exact and near-duplicate docs by normalized text / hash. */
2098
- declare function dedupe(docs: EvidenceDoc[]): EvidenceDoc[];
2099
- /**
2100
- * Adaptive-k: choose how many documents to keep based on the score distribution
2101
- * rather than a fixed cutoff. When the top document dominates, keep fewer; when
2102
- * scores are flat, keep up to the cap. This directly trims input tokens.
2103
- */
2104
- declare function adaptiveK(docs: EvidenceDoc[], opts?: {
2105
- maxDocuments: number;
2106
- minDocuments?: number;
2107
- }): EvidenceDoc[];
2108
- /**
2109
- * Execute a retrieval plan. NO_RETRIEVAL / TOOL_ACTION classes retrieve nothing.
2110
- * Otherwise: retrieve -> dedupe -> rerank -> adaptive-k, bounded by maxRounds.
2111
- */
2112
- declare function executeRetrievalPlan(plan: RetrievalPlan, deps: {
2113
- retriever?: Retriever;
2114
- reranker?: Reranker;
2115
- }): Promise<RetrievalOutcome>;
2116
2129
 
2117
2130
  /**
2118
- * Conversational memory by deltas.
2119
- *
2120
- * A major source of wasted input tokens is resending the entire chat history
2121
- * every turn. Instead we keep a compact structured state and, each turn, apply a
2122
- * small delta produced BY THE MAIN GENERATION (not a separate summarization
2123
- * call). The next prompt receives: compact relevant state + the last 1-3 raw
2124
- * turns + the current message — never the whole conversation.
2125
- */
2126
- interface ConversationState {
2127
- goals: string[];
2128
- confirmedFacts: Record<string, unknown>;
2129
- userPreferences: Record<string, unknown>;
2130
- decisions: string[];
2131
- openItems: string[];
2132
- evidenceRefs: string[];
2133
- }
2134
- interface MemoryDelta {
2135
- set?: Record<string, unknown>;
2136
- append?: Record<string, unknown[]>;
2137
- remove?: string[];
2138
- }
2139
- declare function emptyState(): ConversationState;
2140
- /**
2141
- * Merge a delta into state locally. `set` updates confirmedFacts/preferences,
2142
- * `append` pushes onto list fields, `remove` deletes fact keys or list entries.
2143
- * The merge is deterministic and side-effect-free (returns a new object).
2144
- */
2145
- declare function mergeDelta(state: ConversationState, delta: MemoryDelta): ConversationState;
2146
- /**
2147
- * Project the state + recent turns into the minimal message set to send. Only
2148
- * relevant, non-empty state is serialized, then the last `rawTurns` messages,
2149
- * then the current message.
2150
- */
2151
- declare function projectContext(opts: {
2152
- state: ConversationState;
2153
- history: ChatMessage[];
2154
- current: ChatMessage;
2155
- rawTurns?: number;
2156
- }): ChatMessage[];
2157
- declare function renderState(state: ConversationState): string;
2158
-
2159
- /**
2160
- * A serializer encodes a structured payload into prompt text, and reports the
2161
- * instruction overhead a model needs to *understand* that encoding. The format
2162
- * selector compares `tokens(encode) + tokens(instruction)` across serializers.
2163
- */
2164
- interface Serializer {
2165
- readonly format: SerializationFormat;
2166
- /** Whether this serializer can represent the given value at all. */
2167
- supports(value: unknown): boolean;
2168
- encode(value: unknown): string;
2169
- /**
2170
- * The instruction text a model needs to interpret this format. For JSON this
2171
- * is empty; for row/CSV it is the per-request cost that must be amortized
2172
- * before the format is worthwhile.
2173
- */
2174
- instruction(value: unknown): string;
2175
- /**
2176
- * Decode encoded text back to a value, used ONLY for the selector's round-trip
2177
- * fidelity check. A serializer without a faithful decoder is treated as
2178
- * UNVERIFIABLE: the selector will not choose it (fail-closed), so we never ship
2179
- * — or bill savings for — an encoding we can't prove reproduces the payload.
2180
- * JSON-compact is inherently lossless and is exempt from the check.
2181
- */
2182
- decode?(text: string): unknown;
2183
- }
2184
-
2185
- /**
2186
- * Compact JSON: the lossless baseline every other format is measured against.
2187
- * No indentation, no incidental whitespace. This is frequently the *winner* for
2188
- * deeply nested or non-uniform data, which is exactly why it is the baseline and
2189
- * The compact encoder is only ever a challenger.
2190
- */
2191
- declare const jsonCompactSerializer: Serializer;
2192
-
2193
- /**
2194
- * Compact row encoder.
2195
- *
2196
- * Its strength is UNIFORM ARRAYS OF FLAT OBJECTS: it declares the column
2197
- * names once and then emits one row per object, which removes the repeated keys
2198
- * that make JSON expensive for tabular data. On deeply nested or non-uniform
2199
- * data it can be *larger* than compact JSON — so this is only ever a candidate
2200
- * the selector may pick, never a default.
2201
- *
2202
- * This is an in-house, pinned encoder placed behind the {@link Serializer}
2203
- * interface (as recommended: do not couple the system to an upstream `main`).
2204
- * It emits a well-defined subset and ships a matching decoder so the JSON<->rows
2205
- * round-trip is deterministic and testable.
2206
- */
2207
- interface CompactRowsOptions {
2208
- /** Field delimiter within tabular/inline rows. */
2209
- delimiter?: "," | "\t";
2210
- /** Indentation unit. */
2211
- indent?: string;
2212
- }
2213
- declare function encodeCompactRows(value: unknown, options?: CompactRowsOptions): string;
2214
- /** Return shared column names iff every element is a flat, scalar-valued object with identical keys. */
2215
- declare function uniformColumns(arr: unknown[]): string[] | null;
2216
- /**
2217
- * The instruction a model needs to read the compact encoder. Kept short on purpose — this text
2218
- * is the per-request overhead the selector must amortize before it wins.
2219
- */
2220
- declare const COMPACT_ROWS_INSTRUCTION: string;
2221
- declare function createCompactRowsSerializer(options?: CompactRowsOptions): Serializer;
2222
- declare const compactRowsSerializer: Serializer;
2223
-
2224
- /**
2225
- * Decoder for the subset emitted by {@link encodeCompactRows}.
2226
- *
2227
- * Supports: scalar fields, nested objects, empty arrays, inline scalar arrays
2228
- * (`key[N]: a,b,c`) and tabular object arrays (`key[N]{c1,c2}:` + rows). This
2229
- * covers the cases the format selector actually chooses it for, and makes the
2230
- * JSON<->rows round-trip deterministic and testable. List-form arrays of nested
2231
- * objects are intentionally out of scope for decoding (the selector never picks
2232
- * it for those, since JSON wins there anyway).
2233
- */
2234
- interface CompactRowsDecodeOptions {
2235
- delimiter?: "," | "\t";
2236
- indent?: string;
2237
- }
2238
- declare function decodeCompactRows(text: string, options?: CompactRowsDecodeOptions): unknown;
2239
-
2240
- /**
2241
- * CSV serializer — only valid for genuinely tabular data (a uniform array of
2242
- * flat objects). CSV can be marginally smaller than the compact rows on pure tables, but it
2243
- * does NOT preserve the JSON data model in general, so `supports()` is strict.
2244
- */
2245
- declare const csvSerializer: Serializer;
2246
-
2247
- /**
2248
- * Format selector.
2131
+ * Format selector.
2249
2132
  *
2250
2133
  * It is a candidate, never a default. For each request we count, USING THE
2251
2134
  * DESTINATION MODEL'S TOKENIZER:
@@ -2291,26 +2174,6 @@ declare class FormatSelector {
2291
2174
  }): FormatChoice;
2292
2175
  }
2293
2176
 
2294
- /**
2295
- * Structural validation against the JsonSchema subset the layer uses.
2296
- *
2297
- * Constrained decoding should prevent invalid structures at generation time,
2298
- * but a provider without native support can still return free text. This
2299
- * validator is the deterministic gate that decides whether a response satisfies
2300
- * its output contract — and, when it does not, whether a single repair is
2301
- * warranted. NOTE: structural validity is NOT semantic validity; risky tasks
2302
- * still need a semantic check on top.
2303
- */
2304
- interface ValidationError {
2305
- path: string;
2306
- message: string;
2307
- }
2308
- interface ValidationResult {
2309
- valid: boolean;
2310
- errors: ValidationError[];
2311
- }
2312
- declare function validateAgainstSchema(value: unknown, schema: JsonSchema, path?: string): ValidationResult;
2313
-
2314
2177
  /**
2315
2178
  * Schema registry.
2316
2179
  *
@@ -2498,6 +2361,61 @@ interface CompressionEconomics {
2498
2361
  minimumNetTokens?: number;
2499
2362
  }
2500
2363
 
2364
+ /**
2365
+ * Retrieval planning: retrieve -> dedupe -> rerank -> adaptive-k.
2366
+ *
2367
+ * Eliminating irrelevant context is cheaper and safer than compressing it, so
2368
+ * retrieval prunes aggressively BEFORE anything reaches the prompt. Documents
2369
+ * are kept out of the prompt where possible and passed as references/hashes;
2370
+ * only the selected, trimmed evidence is projected in.
2371
+ */
2372
+ interface EvidenceDoc {
2373
+ id: string;
2374
+ text: string;
2375
+ /** Optional pre-computed relevance score from the retriever. */
2376
+ score?: number;
2377
+ source?: string;
2378
+ /** Content hash, so large docs can be referenced instead of inlined. */
2379
+ hash?: string;
2380
+ }
2381
+ interface Retriever {
2382
+ retrieve(query: string, k: number): Promise<EvidenceDoc[]>;
2383
+ }
2384
+ interface Reranker {
2385
+ rerank(query: string, docs: EvidenceDoc[]): Promise<EvidenceDoc[]>;
2386
+ }
2387
+ interface RetrievalPlan {
2388
+ execution: ExecutionClass;
2389
+ query: string;
2390
+ maxRounds: number;
2391
+ maxDocuments: number;
2392
+ }
2393
+ interface RetrievalOutcome {
2394
+ docs: EvidenceDoc[];
2395
+ rounds: number;
2396
+ retrievedCount: number;
2397
+ keptCount: number;
2398
+ }
2399
+ /** Remove exact and near-duplicate docs by normalized text / hash. */
2400
+ declare function dedupe(docs: EvidenceDoc[]): EvidenceDoc[];
2401
+ /**
2402
+ * Adaptive-k: choose how many documents to keep based on the score distribution
2403
+ * rather than a fixed cutoff. When the top document dominates, keep fewer; when
2404
+ * scores are flat, keep up to the cap. This directly trims input tokens.
2405
+ */
2406
+ declare function adaptiveK(docs: EvidenceDoc[], opts?: {
2407
+ maxDocuments: number;
2408
+ minDocuments?: number;
2409
+ }): EvidenceDoc[];
2410
+ /**
2411
+ * Execute a retrieval plan. NO_RETRIEVAL / TOOL_ACTION classes retrieve nothing.
2412
+ * Otherwise: retrieve -> dedupe -> rerank -> adaptive-k, bounded by maxRounds.
2413
+ */
2414
+ declare function executeRetrievalPlan(plan: RetrievalPlan, deps: {
2415
+ retriever?: Retriever;
2416
+ reranker?: Reranker;
2417
+ }): Promise<RetrievalOutcome>;
2418
+
2501
2419
  /**
2502
2420
  * Provider-neutral generation interface. Every provider (OpenAI-compatible,
2503
2421
  * Anthropic-compatible, Google-compatible, self-hosted) is adapted to this
@@ -2544,6 +2462,12 @@ interface ProviderRequest {
2544
2462
  * {@link ProviderResponse.servedServiceTier}.
2545
2463
  */
2546
2464
  deferrable?: boolean;
2465
+ /**
2466
+ * Adapters with explicit prompt-cache control write the provider's cache only
2467
+ * at `stablePrefixMessages` (nowhere when it is undefined) instead of
2468
+ * letting the provider choose. Others ignore it.
2469
+ */
2470
+ explicitCacheWrites?: boolean;
2547
2471
  }
2548
2472
  interface ProviderUsage {
2549
2473
  inputTokens: number;
@@ -2552,7 +2476,9 @@ interface ProviderUsage {
2552
2476
  cachedInputTokens?: number;
2553
2477
  /**
2554
2478
  * Input tokens billed at a PREMIUM to WRITE the provider's prefix cache
2555
- * (Anthropic `cache_creation_input_tokens`: 1.25x the normal rate).
2479
+ * (Anthropic `cache_creation_input_tokens`, and OpenAI
2480
+ * `prompt_tokens_details.cache_write_tokens` on GPT-5.6 and later: 1.25x the
2481
+ * normal rate).
2556
2482
  *
2557
2483
  * This is a COST, not a discount, and it is reported separately for exactly
2558
2484
  * that reason: `cachedInputTokens` is a saving to publish in ledger 4, this
@@ -2640,6 +2566,15 @@ interface ProviderResponse {
2640
2566
  * would be describing a request some other adapter never made.
2641
2567
  */
2642
2568
  requestedServiceTier?: string;
2569
+ /**
2570
+ * Whether this answer can be handed to a later identical request (exact cache) or to a
2571
+ * concurrent identical request. `false` when it carries content the layer keeps no copy of,
2572
+ * such as generated files, cited sources, results of tools the provider ran itself, or
2573
+ * reasoning the provider expects back: it is served to this caller only, it is not written
2574
+ * to the exact cache, and a concurrent identical request makes its own call. Absent means
2575
+ * replayable.
2576
+ */
2577
+ replayable?: boolean;
2643
2578
  raw?: unknown;
2644
2579
  }
2645
2580
  /** One event of a provider-level stream. */
@@ -2671,1140 +2606,1565 @@ interface Provider {
2671
2606
  }
2672
2607
 
2673
2608
  /**
2674
- * Deterministic mock provider.
2609
+ * Offline license verification for the paid SDK.
2675
2610
  *
2676
- * Lets the whole layer run — and be tested — with no network and no
2677
- * credentials. When a schema is supplied it fabricates a schema-valid object,
2678
- * demonstrating the constrained-decoding contract end to end. Token usage is
2679
- * derived from the heuristic estimator so SavingsReport numbers are meaningful
2680
- * in examples and tests.
2611
+ * A license is a compact signed token `base64url(header).base64url(payload).
2612
+ * base64url(sig)`, the exact format BiVelio's licensing service mints. We verify
2613
+ * the Ed25519 signature against BiVelio's public key, decode the payload, and
2614
+ * apply the same grace policy the issuer applies. Verification is
2615
+ * **browser-safe**: it uses
2616
+ * `@noble/curves` (pure JS) + Web primitives (`atob`, `TextEncoder`) rather than
2617
+ * `node:crypto`, so the shared entry point still compiles into the browser
2618
+ * demo bundle. The private signing key never leaves the control plane; only the
2619
+ * public key is embedded here, so a token cannot be forged or tampered with.
2620
+ */
2621
+ /**
2622
+ * `stale` NO es una licencia caducada: es una licencia PAGADA Y VIGENTE que no ha
2623
+ * podido confirmar su estado contra el control plane dentro de la ventana de
2624
+ * frescura — típicamente porque la salida de red está bloqueada.
2681
2625
  *
2682
- * ⚠️ WHAT THIS PROVIDER CANNOT MEASURE, and it has already cost money once.
2683
- * It fabricates the answer FROM THE SCHEMA it receives, so a smaller schema
2684
- * always produces a smaller answer. Any lever that shrinks the contract sent to
2685
- * the provider therefore appears to shrink the OUTPUT too — automatically, on
2686
- * every run, whatever model id is being simulated. That is a property of this
2687
- * file, not of any model.
2626
+ * Existe porque antes ese caso reutilizaba `expired` "para que el union y todos
2627
+ * los consumidores siguieran igual", y el ahorro de tipo se pagaba con el peor
2628
+ * mensaje posible del producto: a un cliente al día, cuyo único problema era un
2629
+ * cortafuegos, se le decía *"license is not active (expired). Renew your
2630
+ * subscription"*. Le mandábamos a pagar otra vez algo que ya había pagado, y a
2631
+ * mirar donde no estaba el problema.
2688
2632
  *
2689
- * The output codebook is exactly that shape of lever, and against this provider
2690
- * the benchmark reported it saving 24.8%. Measured live it saves 8.0% on
2691
- * `gpt-4o` and COSTS 8.2% on `gpt-4o-mini`, because a real model decides how
2692
- * much to WRITE and a smaller model writes more when the field names carry less
2693
- * meaning. So: never take an output-token saving measured here as evidence of
2694
- * anything. Prove those with the paired A/B in `benchmarks/ab-live.ts`, and
2695
- * write tests for output-side effects against a provider that models the
2696
- * behaviour you are claiming — see `test/codebook-de-salida.test.ts`.
2697
- */
2698
- interface MockProviderOptions {
2699
- /** Fixed text to return when no schema is present. */
2700
- answer?: string;
2701
- /** Override the fabricated structured object. */
2702
- data?: unknown;
2703
- /** Simulated provider-cached input tokens (ledger 4). */
2704
- cachedInputTokens?: number;
2705
- /** Characters per text event in generateStream (default 8). */
2706
- streamChunkChars?: number;
2633
+ * El comportamiento no cambia —sigue siendo fail-closed, `granted()` no lo
2634
+ * concede— pero ahora el mensaje puede decir la verdad.
2635
+ *
2636
+ * `none` means "there is no key": what is left after `normalizeLicenseKey` is
2637
+ * EMPTY. It is the same value `LicenseError.status` already carries when nothing
2638
+ * was configured, because it is the same problem with the same fix: a
2639
+ * `licenseKey: " "` (an empty environment variable, a blank secret file) used
2640
+ * to tell the customer their key "does not verify" — there was no key.
2641
+ */
2642
+ type LicenseStatus = "active" | "grace" | "expired" | "invalid" | "revoked" | "stale" | "none";
2643
+ /** Decoded license token body (mirror of `@bivelio/protocol`'s LicensePayload,
2644
+ * declared locally so the SDK stays free of a control-plane dependency). */
2645
+ interface LicenseTokenPayload {
2646
+ jti: string;
2647
+ org_id: string;
2648
+ plan: string;
2649
+ seats: number;
2650
+ features: string[];
2651
+ issued_at: number;
2652
+ expires_at: number;
2653
+ grace_days: number;
2654
+ iss?: string;
2707
2655
  }
2708
- declare class MockProvider implements Provider {
2709
- readonly id = "mock";
2710
- private readonly options;
2711
- constructor(options?: MockProviderOptions);
2712
- generate(req: ProviderRequest, _options?: ProviderStreamOptions): Promise<ProviderResponse>;
2713
- /**
2714
- * Streaming variant of generate(): the same answer, cut into text events of
2715
- * `streamChunkChars` characters, then one `done` with the generate() response.
2716
- */
2717
- generateStream(req: ProviderRequest): AsyncGenerator<ProviderStreamEvent, void, undefined>;
2656
+ interface ResolvedLicense {
2657
+ status: LicenseStatus;
2658
+ payload?: LicenseTokenPayload;
2659
+ reason?: string;
2660
+ }
2661
+ declare const EMBEDDED_PUBLIC_KEY: string;
2662
+ /** Result of verifying a compact `head.body.sig` token's signature. */
2663
+ interface CompactVerification {
2664
+ /** True only if the signature is valid for the given public key. */
2665
+ ok: boolean;
2666
+ /** Decoded header (present when `ok`), e.g. `{ alg, typ, v }`. */
2667
+ header?: Record<string, unknown>;
2668
+ /** Raw payload bytes (present when `ok`); the caller decodes/parses them. */
2669
+ payloadBytes?: Uint8Array;
2670
+ reason?: string;
2718
2671
  }
2719
- declare function createMockProvider(options?: MockProviderOptions): Provider;
2720
-
2721
2672
  /**
2722
- * OpenAI-compatible provider adapter (chat/completions shape).
2673
+ * A pinned set of Ed25519 public keys addressed by key-id (`kid`), enabling signing
2674
+ * key ROTATION without breaking already-deployed SDKs: new keys are added as extra
2675
+ * map entries, and tokens carry the `kid` they were signed with. `defaultKid` is the
2676
+ * key used for legacy tokens that carry NO `kid` (so today's single-key tokens keep
2677
+ * verifying unchanged). Verification FAILS CLOSED on a `kid` not in the map.
2678
+ */
2679
+ interface LicenseKeyset {
2680
+ /** kid → SPKI PEM. */
2681
+ keys: Record<string, string>;
2682
+ /** kid used for tokens without a `kid` header (the current/legacy key). */
2683
+ defaultKid: string;
2684
+ }
2685
+ /** Narrow a verification key to a {@link LicenseKeyset} (vs a bare SPKI PEM string). */
2686
+ declare function isKeyset(k: string | LicenseKeyset): k is LicenseKeyset;
2687
+ /**
2688
+ * Normalize a license key the way it arrives from the real world — ONCE, at the
2689
+ * entry point, never inside the verifier.
2723
2690
  *
2724
- * Works with any endpoint that speaks the OpenAI Chat Completions API — OpenAI,
2725
- * Azure, vLLM, SGLang, TGI, a local gateway, or Vercel AI Gateway with a
2726
- * "provider/model" string. Structured output is requested via `response_format`
2727
- * with a JSON schema when a contract is present, which is the provider-native
2728
- * constrained-decoding path preferred by the layer.
2691
+ * The README says `export BIVELIO_LICENSE_KEY=…` and dotenv trims, so that path
2692
+ * never showed it. But anyone reading the key from a file (`readFileSync`), from
2693
+ * a Docker/Kubernetes secret (mounted as a file, trailing newline included) or
2694
+ * copying it out of an `Authorization` header handed over a CORRECT key with a
2695
+ * `\n` after it, a space before it, quotes around it or a `Bearer ` in front —
2696
+ * and the SDK answered "does not verify, check you pasted the WHOLE key". A
2697
+ * wrong diagnosis on a good key is the worst possible hint: it sends the
2698
+ * customer to look where the problem is not.
2729
2699
  *
2730
- * Models that predate structured outputs reject `response_format` with a 400. The
2731
- * adapter degrades for those: it repeats the request once with the schema carried
2732
- * in the prompt instead, remembers the model so later requests skip the rejected
2733
- * attempt, and reports `structuredNatively: false` so the savings report states
2734
- * `constraints.backend: "none"` rather than claiming a guarantee the provider
2735
- * never gave.
2700
+ * Worse, it was INCONSISTENT: `tok + "\r\n"` passed and `tok + "\n"` did not,
2701
+ * because `atob` ignores ASCII whitespace while the padding was computed from a
2702
+ * length that included it, and two extra characters give the same remainder
2703
+ * modulo 4 as zero. Behavior that depends on which editor saved the file is not
2704
+ * behavior.
2736
2705
  *
2737
- * Network calls use global `fetch` (Node >= 18) so there is no SDK dependency.
2706
+ * Only OUTER wrapping is removed — whitespace, one pair of single or double
2707
+ * quotes, a `Bearer` prefix in any capitalization — in that order, in one pass.
2708
+ * Anything INSIDE the key is left alone: an interior space is not wrapping, it
2709
+ * is a broken key, and {@link licenseKeyShape} reports it as `malformed`.
2738
2710
  */
2739
- interface OpenAICompatibleOptions {
2740
- baseUrl: string;
2741
- apiKey?: string;
2742
- /**
2743
- * Strip the `provider/` prefix from model ids before sending (`openai/gpt-4o-mini`
2744
- * → `gpt-4o-mini`).
2745
- *
2746
- * Default (undefined): strip when `baseUrl` is OpenAI's own API or an Azure
2747
- * OpenAI endpoint, keep otherwise. OpenAI rejects a prefixed id with a 404
2748
- * ("model does not exist"), so the qualified ids the layer uses everywhere
2749
- * else — pricing, cache keys, the report — would break the very first call
2750
- * that reaches the provider. A gateway or a self-hosted server with a
2751
- * namespaced model name usually WANTS the prefix, so it is kept there. Set `true`/`false` to decide explicitly; an explicit value
2752
- * always wins.
2753
- */
2754
- stripProviderPrefix?: boolean;
2755
- /** Extra headers (e.g. for a gateway). */
2756
- headers?: Record<string, string>;
2757
- /** Injectable fetch for testing. */
2758
- fetchImpl?: typeof fetch;
2759
- /**
2760
- * When true (recommended for measurement), a response whose `usage` block is
2761
- * missing or reports zero prompt/completion tokens for a call that actually
2762
- * ran is treated as a HARD ERROR instead of being silently coerced to
2763
- * 0 tokens. This closes the single most dishonest failure mode: an absent
2764
- * usage payload otherwise becomes `actual cost = 0` → a fabricated 100%
2765
- * saving. Benchmarks that make real cost claims must set this.
2766
- */
2767
- requireUsage?: boolean;
2768
- }
2711
+ declare function normalizeLicenseKey(raw: string): string;
2769
2712
  /**
2770
- * ¿Cumple este esquema las reglas del modo `strict` de OpenAI?
2713
+ * The SHAPE of an already-normalized key, so the error message can tell apart
2714
+ * three problems that used to get the same text:
2771
2715
  *
2772
- * Son dos, y aplican a TODO objeto anidado:
2773
- * 1. `additionalProperties: false`.
2774
- * 2. `required` tiene que listar TODAS las claves de `properties` — no solo
2775
- * las obligatorias. En modo strict no existe el campo opcional.
2716
+ * · `empty` — there is no key (only whitespace, or nothing at all).
2717
+ * · `malformed` — not even the shape of a token: not three dot-separated
2718
+ * base64url segments. This is where the `bvk_` service key
2719
+ * pasted where the license goes lands, as does arbitrary text
2720
+ * or a token with a line break in the middle.
2721
+ * · `token` — has the shape; whether it verifies is up to the signature.
2776
2722
  *
2777
- * Un esquema perfectamente válido como
2778
- * { properties: { answer, refs }, required: ["answer"] }
2779
- * incumple la segunda y el proveedor responde 400. No es un fallo del cliente:
2780
- * es una restricción de OpenAI que JSON Schema no tiene.
2723
+ * It is the SAME shape rule {@link verifyCompactSignature} enforces, so that "has
2724
+ * the shape" and "verification was attempted" mean the same thing.
2781
2725
  */
2782
- declare function isStrictCompatible(schema: unknown): boolean;
2726
+ type LicenseKeyShape = "empty" | "malformed" | "token";
2727
+ /** Classify a normalized key. See {@link LicenseKeyShape}. */
2728
+ declare function licenseKeyShape(normalizedKey: string): LicenseKeyShape;
2783
2729
  /**
2784
- * Decides the id that travels on the wire. Explicit option wins; otherwise the
2785
- * endpoint decides: OpenAI/Azure get the bare id, everything else keeps what
2786
- * the caller wrote. Exported for the pipeline-level test that pins the README
2787
- * snippet; not part of the public API.
2730
+ * Verify the Ed25519 signature of any BV-SALA compact token
2731
+ * (`base64url(header).base64url(body).base64url(sig)`, sig over `${head}.${body}`)
2732
+ * and return the decoded header + raw payload bytes. This is the single shared
2733
+ * crypto primitive: license tokens AND signed revocation lists both go through it,
2734
+ * so the browser (`@noble/curves`) and node (`node:crypto`) sides agree byte-for-byte
2735
+ * and there is no bespoke per-message verification path to keep in sync. It does NOT
2736
+ * interpret the payload or the header `typ` — callers do that.
2737
+ *
2738
+ * Shape is checked SEGMENT by segment, not just by counting dots. Three parts
2739
+ * used to be enough and the rest was left to `atob`, which ignores whitespace:
2740
+ * that is how a token with `\r\n` after it verified while the same token with
2741
+ * `\n` did not (see {@link normalizeLicenseKey}). The shared primitive must give
2742
+ * the same answer to the same input, always; a caller that wants to tolerate
2743
+ * wrapping normalizes BEFORE calling.
2788
2744
  */
2789
- declare function quitarPrefijoDeProveedor(model: string, explicito: boolean | undefined, baseUrl: string): string;
2790
- declare class OpenAICompatibleProvider implements Provider {
2791
- readonly id: string;
2792
- private readonly opts;
2793
- private readonly fetchImpl;
2794
- /** Models of this endpoint already known to reject `response_format`. */
2795
- private readonly sinResponseFormat;
2796
- private streamOptionsRejected;
2797
- constructor(opts: OpenAICompatibleOptions);
2798
- generate(req: ProviderRequest, options?: ProviderStreamOptions): Promise<ProviderResponse>;
2799
- /**
2800
- * Streaming variant of generate() over Chat Completions. It always asks for
2801
- * the final usage chunk (`stream_options.include_usage`); an endpoint that
2802
- * rejects that option is retried once without it and remembered, and its
2803
- * streamed answers then report `usage.complete: false`. With `requireUsage`,
2804
- * that rejection throws instead. Requests with an output contract or a
2805
- * deferrable tier are not streamed: they are served by generate() and yielded
2806
- * as one text event.
2807
- */
2808
- generateStream(req: ProviderRequest, options?: ProviderStreamOptions): AsyncGenerator<ProviderStreamEvent, void, undefined>;
2809
- private enviar;
2810
- /** Translate a 2xx endpoint response into the provider-neutral shape. */
2811
- private leer;
2812
- }
2813
-
2745
+ declare function verifyCompactSignature(token: string, publicKey: string | LicenseKeyset): CompactVerification;
2746
+ /** Verify a token's signature and decode its payload. Does not check expiry. */
2747
+ declare function verifyLicenseToken(token: string, publicKey: string | LicenseKeyset): {
2748
+ signatureValid: boolean;
2749
+ payload?: LicenseTokenPayload;
2750
+ reason?: string;
2751
+ };
2814
2752
  /**
2815
- * Anthropic Messages API adapter.
2816
- *
2817
- * Speaks `POST /v1/messages`: the system prompt travels in its own top-level
2818
- * `system` field (not as a message), tools are `{name, description,
2819
- * input_schema}` with no `function` wrapper, and prefix caching is requested
2820
- * with EXPLICIT `cache_control` breakpoints — Anthropic does not cache on its
2821
- * own. A cached read is billed at 0.1x the input rate, which is the largest
2822
- * single lever on the input side of a customer's bill.
2753
+ * Apply the grace + revocation policy to an already-decoded payload → a status.
2754
+ * Pure and clock-injected, so a LIVE gate can call it on every request to pick up
2755
+ * both revocation (jti in the set) AND expiry transitions without re-verifying the
2756
+ * signature. Revocation takes precedence over the time-based status.
2757
+ * jti on the revocation set → revoked
2758
+ * now <= expires_at → active
2759
+ * expires_at < now <= +grace_days → grace (still entitled)
2760
+ * now > expires_at + grace_days → expired
2761
+ */
2762
+ declare function deriveStatus(payload: {
2763
+ jti: string;
2764
+ expires_at: number;
2765
+ grace_days: number;
2766
+ }, nowSeconds: number, revoked?: ReadonlySet<string>): LicenseStatus;
2767
+ /**
2768
+ * Verify + apply the grace policy → a resolved status. Mirrors what the issuing
2769
+ * control plane resolves for the same token. Bad signature / malformed
2770
+ * → invalid; otherwise the status comes from {@link deriveStatus}.
2823
2771
  *
2824
- * Network calls use global `fetch` (Node >= 18) so there is no SDK dependency.
2772
+ * A public entry point, so it normalizes like the others (gate and refresher):
2773
+ * the same key must resolve the same whichever way it comes in.
2825
2774
  */
2826
- /** Render order of the request, which is also the order the cache prefix grows in. */
2827
- interface AnthropicOptions {
2828
- apiKey?: string;
2829
- /** Defaults to the public API. Override for a gateway or a proxy. */
2830
- baseUrl?: string;
2775
+ declare function resolveLicense(token: string, publicKey: string | LicenseKeyset, opts?: {
2776
+ nowSeconds?: number;
2777
+ revoked?: ReadonlySet<string>;
2778
+ }): ResolvedLicense;
2779
+
2780
+ /**
2781
+ * Everything the pipeline depends on. All fields except `provider` and
2782
+ * `tokenizer` are optional; sensible in-memory defaults are wired by
2783
+ * {@link createSavingsLayer} so the layer works out of the box and every
2784
+ * component is independently swappable.
2785
+ */
2786
+ interface SavingsLayerConfig {
2831
2787
  /**
2832
- * Strip the `provider/` prefix from model ids before sending
2833
- * (`anthropic/claude-sonnet-4-5` → `claude-sonnet-4-5`). Default (undefined):
2834
- * strip when talking to `api.anthropic.com`, which rejects a prefixed id;
2835
- * keep for a gateway or proxy, which usually routes on it. An explicit value
2836
- * always wins.
2837
- */
2838
- stripProviderPrefix?: boolean;
2839
- /** Wire version pinned in the `anthropic-version` header. */
2840
- apiVersion?: string;
2841
- /** Extra headers (e.g. for a gateway). */
2842
- headers?: Record<string, string>;
2843
- /** Injectable fetch for testing. */
2844
- fetchImpl?: typeof fetch;
2788
+ * Contratos a los que ya se les incumplió una restricción de valor.
2789
+ *
2790
+ * La capa lo aprende sola, pero lo aprendido muere con la instancia. En
2791
+ * serverless —una instancia por petición— eso significa no aprender NUNCA:
2792
+ * cada llamada estrena y paga su reparación. Pasar aquí un conjunto
2793
+ * compartido (o respaldado por Redis, con la API de Set) hace que se aprenda
2794
+ * una vez para todo el despliegue.
2795
+ *
2796
+ * Solo guarda identificadores de contrato, nunca contenido de peticiones.
2797
+ */
2798
+ constraintMemory?: Set<string>;
2799
+ provider: Provider;
2800
+ tokenizer: TokenizerRegistry;
2801
+ exactCache?: ExactCache;
2802
+ semanticCache?: InMemorySemanticCache;
2803
+ singleflight?: Singleflight;
2804
+ resolvers?: ResolverRegistry;
2805
+ router?: ExecutionRouter;
2806
+ toolSelector?: ToolSelector;
2807
+ formatSelector?: FormatSelector;
2808
+ schemaRegistry?: SchemaRegistry;
2809
+ constraintBackends?: ConstraintBackend[];
2810
+ compressor?: Compressor;
2845
2811
  /**
2846
- * `max_tokens` is REQUIRED by this API — unlike the OpenAI shape, where it is
2847
- * optional. A request without one is a 400, so the adapter has to supply a
2848
- * value. It is deliberately generous: a low default would truncate answers
2849
- * and the truncation would look like a model failure rather than an adapter
2850
- * default.
2812
+ * The numbers behind "compress only when it pays for itself" — in money, not
2813
+ * in tokens.
2814
+ *
2815
+ * All optional, and the default never invents a saving: the layer compresses
2816
+ * when the net token saving justifies it, and holds back when compressing
2817
+ * would push the prompt below the provider's cacheable minimum — where fewer
2818
+ * tokens can mean a larger bill. Declare {@link CompressionEconomics.prefix}
2819
+ * with your own measurements to unlock the full decision in both directions.
2851
2820
  */
2852
- defaultMaxOutputTokens?: number;
2821
+ compressionEconomics?: CompressionEconomics;
2822
+ retriever?: Retriever;
2823
+ reranker?: Reranker;
2853
2824
  /**
2854
- * When true (recommended for measurement), a response whose `usage` block is
2855
- * missing its counters is treated as a HARD ERROR instead of being coerced to
2856
- * 0 tokens. An absent usage payload otherwise becomes `actual cost = 0` → a
2857
- * fabricated 100% saving.
2825
+ * Tuning for the model cascade (`savings.modelCascade`). All optional; the
2826
+ * defaults measure and never spend.
2858
2827
  */
2859
- requireUsage?: boolean;
2828
+ modelCascade?: ModelCascadeSettings;
2860
2829
  /**
2861
- * How many `cache_control` breakpoints the adapter may spend.
2862
- *
2863
- * The API caps them at 4 per request. The adapter never needs more than 2
2864
- * (end of system, end of the stable message prefix) and the cap is exposed so
2865
- * a gateway with a stricter limit can lower it. Raising it above 4 is
2866
- * pointless: the extra breakpoints are rejected by the API, not ignored.
2830
+ * Layer-wide defaults for the provider price levers (`savings.deferrable`,
2831
+ * `savings.standalone`) and what to do when the cheaper tier has no capacity.
2832
+ * All off by default; a request's own `savings` value always wins.
2867
2833
  */
2868
- maxCacheBreakpoints?: number;
2834
+ priceLevers?: PriceLeverSettings;
2869
2835
  /**
2870
- * When true, requests that the pipeline decided to mark also carry the
2871
- * request-level `cache_control` (Anthropic's automatic caching): the provider
2872
- * places one extra breakpoint on the last cacheable block and moves it
2873
- * forward as the conversation grows, so the history beyond the declared
2874
- * stable prefix is cached incrementally too. It spends one of the four
2875
- * breakpoint slots and never fires when the economic gate declined to mark.
2836
+ * Signed BiVelio license token (from your paid subscription). This is the
2837
+ * primary way to license the SDK: `createSavingsLayer` verifies it offline
2838
+ * against the embedded public key and builds a {@link licenseGate} from it.
2839
+ * Without a valid, active license (and without {@link devUnlicensed}), the
2840
+ * pipeline refuses to run — get a key at https://savings.bivelio.com.
2876
2841
  *
2877
- * Off by default, and deliberately so: the moving breakpoint writes the
2878
- * volatile tail of EVERY marked request at the 1.25x write premium. That
2879
- * pays for itself on conversational or agentic traffic — the next turn reads
2880
- * the whole history at 0.1x — and loses money on one-shot traffic where
2881
- * nothing ever reads it back. It is also rejected by legacy Bedrock
2882
- * deployments. Turn it on when your traffic is multi-turn.
2842
+ * Surrounding whitespace (the trailing newline of a file or mounted secret),
2843
+ * wrapping quotes and a `Bearer ` prefix are stripped before verification, so
2844
+ * a key read with `readFileSync` works as-is. Anything INSIDE the key is not
2845
+ * touched: a key with a space in the middle is a broken key.
2883
2846
  */
2884
- automaticCache?: boolean;
2847
+ licenseKey?: string;
2885
2848
  /**
2886
- * Whether a request carrying BOTH the caller's tools and an output contract
2887
- * may send `tool_choice: {type: "any"}`, which obliges the model to answer
2888
- * through SOME tool — its pick — instead of writing prose.
2849
+ * Override the public key used to verify {@link licenseKey}. An SPKI PEM string,
2850
+ * or a kid-addressed {@link LicenseKeyset} (`{ keys: {kid: pem}, defaultKid }`)
2851
+ * to support signing-key rotation — legacy no-kid tokens use `defaultKid`, and
2852
+ * an unknown kid fails closed. For self-hosting/self-testing; defaults to the
2853
+ * build-embedded BiVelio key.
2854
+ */
2855
+ licensePublicKey?: string | LicenseKeyset;
2856
+ /**
2857
+ * Pre-built license gate. **Ignored in the published npm package**, where a
2858
+ * {@link licenseKey} verified against the embedded BiVelio public key is the
2859
+ * only accepted license — passing a gate there logs a warning and changes
2860
+ * nothing. It exists for builds from source (this repo's own tests and
2861
+ * benchmarks), where it takes precedence over {@link licenseKey}.
2862
+ */
2863
+ licenseGate?: LicenseGate;
2864
+ /**
2865
+ * OPT-IN online revocation call-home. When set (alongside {@link licenseKey} and
2866
+ * with no explicit {@link licenseGate}), the SDK polls this base's
2867
+ * `GET /licenses/revocations` for a signed revocation list and enforces it via a
2868
+ * LIVE gate — a license cancelled mid-term stops working without a restart.
2889
2869
  *
2890
- * Defaults to `true`, and the default is the point: without it this exact
2891
- * combination measured 62% MORE expensive than sending no layer at all, every
2892
- * single time, because prose forces a second full call to repair. Turning it
2893
- * off restores the old behaviour for a caller who genuinely needs a plain-text
2894
- * turn to be reachable while a contract is attached.
2870
+ * NON-BLOCKING: the customer request path never blocks on the fetch; the warm poll
2871
+ * is fired but not awaited, and any failed/unreachable poll is a no-op that keeps
2872
+ * the last-known-good snapshot. Leaving this unset keeps the fully-offline,
2873
+ * zero-network default path. Pass the base URL of the licensing service, e.g.
2874
+ * `https://savings.bivelio.com`.
2895
2875
  */
2896
- forceAnswerThroughTool?: boolean;
2897
- }
2898
- declare class AnthropicProvider implements Provider {
2899
- readonly id: string;
2900
- private readonly opts;
2901
- private readonly fetchImpl;
2902
- constructor(opts?: AnthropicOptions);
2903
- /** The id Anthropic's API sees: bare at `api.anthropic.com`, as written elsewhere. */
2904
- private modeloEnElCable;
2905
- generate(req: ProviderRequest, options?: ProviderStreamOptions): Promise<ProviderResponse>;
2876
+ licenseServerUrl?: string;
2906
2877
  /**
2907
- * Streaming variant of generate() over the Messages API. The final usage is
2908
- * read from the closing `message_delta` event (the output counter in
2909
- * `message_start` is provisional); a stream without it reports
2910
- * `usage.complete: false`, and with `requireUsage` it throws instead. A body
2911
- * that ends before the provider signalled completion throws. Requests with an
2912
- * output contract or declared deferrable are served by generate() and yielded
2913
- * as one text event.
2878
+ * Steady-state ceiling between revocation polls, in seconds (default 900 = 15 min).
2879
+ * The effective cadence tightens near license expiry and has a hard floor.
2914
2880
  */
2915
- generateStream(req: ProviderRequest, options?: ProviderStreamOptions): AsyncGenerator<ProviderStreamEvent, void, undefined>;
2916
- private enviar;
2917
- private construirCuerpo;
2918
- private marcarCache;
2919
- private leer;
2920
- }
2921
-
2922
- /**
2923
- * Trace collector: an ordered log of pipeline decisions with elapsed timings.
2924
- * Every stage records why it did what it did, so a SavingsReport can be audited
2925
- * and so ablations can attribute savings to a specific module.
2926
- */
2927
- declare class TraceCollector {
2928
- private readonly events;
2929
- private readonly start;
2930
- private readonly now;
2931
- constructor(now?: () => number);
2932
- record(stage: string, outcome: string, detail?: Record<string, unknown>): void;
2933
- elapsed(): number;
2934
- all(): TraceEvent[];
2935
- }
2936
-
2937
- /**
2938
- * Thrown when the pipeline is asked to run without a valid, active BiVelio
2939
- * license. This is the fail-closed contract: a paid SDK must refuse to operate
2940
- * unlicensed, rather than silently serving requests for free.
2941
- */
2942
- declare class LicenseError extends Error {
2881
+ licensePollIntervalSeconds?: number;
2882
+ /** Injectable `fetch` for the revocation poller (defaults to the global). Testing/proxy. */
2883
+ licenseFetch?: typeof fetch;
2884
+ /** Advanced tuning of the revocation poller cadence + skew tolerance. */
2885
+ licenseRevocation?: {
2886
+ /** Hard floor between polls, seconds (default 300). */
2887
+ floorSeconds?: number;
2888
+ /** Backoff ceiling during a control-plane outage, seconds (default 3600). */
2889
+ maxIntervalSeconds?: number;
2890
+ /** Jitter fraction ± (default 0.1). */
2891
+ jitterRatio?: number;
2892
+ /** Reject a signed list whose `asOf` is more than this far in the future, seconds (default 300). */
2893
+ maxSkewSeconds?: number;
2894
+ /**
2895
+ * ONLINE staleness ceiling, seconds (default 72h, `DEFAULT_MAX_STALENESS_SECONDS`). If the SDK goes this long
2896
+ * without a CONFIRMED authorized heartbeat (a 304 or a verified list), the LIVE
2897
+ * gate stops granting — closing the fail-open window where blocking egress keeps
2898
+ * a revoked license alive until natural expiry. Must exceed the poll cadence
2899
+ * with margin. Applies ONLY on the online path ({@link licenseServerUrl} set);
2900
+ * it is NOT a zero-network opt-out — use the offline path for that.
2901
+ */
2902
+ maxStalenessSeconds?: number;
2903
+ };
2943
2904
  /**
2944
- * Resolved license status — the stable handle for branching on the cause,
2945
- * since the message text is rewritten whenever it can be made clearer:
2905
+ * Service-account key (`bvk_…`) — your machine credential. It does TWO things,
2906
+ * and the second one is why a long-running deployment needs it:
2946
2907
  *
2947
- * - `none` — nothing was configured (no `licenseKey`, no gate).
2948
- * - `invalid` — a key was configured but does not verify.
2949
- * - `expired` / `grace` — it verifies, but the term is over.
2950
- * - `revoked` — cancelled by the control plane.
2951
- * - `stale` — paid and current, but unconfirmed: a connectivity problem.
2908
+ * 1. **Licence renewal pickup.** Each billing period mints a NEW licence token;
2909
+ * the one you deployed expires at the end of the period (+ grace). With this
2910
+ * key the SDK fetches the renewed token from the control plane and swaps it
2911
+ * in without a restart. WITHOUT it there is no machine pickup, so somebody
2912
+ * must paste the new token by hand every cycle or the deployment stops with
2913
+ * `LicenseError` once the grace period ends — while the subscription is paid
2914
+ * and current. Disable with {@link licenseRefresh}`.enabled = false`.
2915
+ * 2. **DETECTIVE usage metering.** Usage figures after each request —
2916
+ * request id, model id, cache outcome, token counts and cost figures, tagged
2917
+ * with an opaque machine seat id (section 5 of the LICENSE lists every field) —
2918
+ * NEVER prompts, completions or provider credentials. Disable with
2919
+ * {@link metering}`.enabled = false` (which does NOT disable renewal pickup).
2952
2920
  *
2953
- * The same value is repeated at the end of the message (`license status: X`)
2954
- * because an uncaught error prints nothing else.
2921
+ * Both are best-effort and non-blocking: a slow or unreachable control plane
2922
+ * never blocks or breaks `generate()`. Get the key from your dashboard.
2955
2923
  */
2956
- readonly status?: string;
2957
- constructor(message: string, status?: string);
2958
- }
2959
-
2960
- /**
2961
- * Offline license verification for the paid SDK.
2962
- *
2963
- * A license is a compact signed token `base64url(header).base64url(payload).
2964
- * base64url(sig)`, the exact format BiVelio's licensing service mints. We verify
2965
- * the Ed25519 signature against BiVelio's public key, decode the payload, and
2966
- * apply the same grace policy the issuer applies. Verification is
2967
- * **browser-safe**: it uses
2968
- * `@noble/curves` (pure JS) + Web primitives (`atob`, `TextEncoder`) rather than
2969
- * `node:crypto`, so the shared entry point still compiles into the browser
2970
- * demo bundle. The private signing key never leaves the control plane; only the
2971
- * public key is embedded here, so a token cannot be forged or tampered with.
2972
- */
2973
- /**
2974
- * `stale` NO es una licencia caducada: es una licencia PAGADA Y VIGENTE que no ha
2975
- * podido confirmar su estado contra el control plane dentro de la ventana de
2976
- * frescura — típicamente porque la salida de red está bloqueada.
2977
- *
2978
- * Existe porque antes ese caso reutilizaba `expired` "para que el union y todos
2979
- * los consumidores siguieran igual", y el ahorro de tipo se pagaba con el peor
2980
- * mensaje posible del producto: a un cliente al día, cuyo único problema era un
2981
- * cortafuegos, se le decía *"license is not active (expired). Renew your
2982
- * subscription"*. Le mandábamos a pagar otra vez algo que ya había pagado, y a
2983
- * mirar donde no estaba el problema.
2984
- *
2985
- * El comportamiento no cambia —sigue siendo fail-closed, `granted()` no lo
2986
- * concede— pero ahora el mensaje puede decir la verdad.
2987
- *
2988
- * `none` means "there is no key": what is left after `normalizeLicenseKey` is
2989
- * EMPTY. It is the same value `LicenseError.status` already carries when nothing
2990
- * was configured, because it is the same problem with the same fix: a
2991
- * `licenseKey: " "` (an empty environment variable, a blank secret file) used
2992
- * to tell the customer their key "does not verify" — there was no key.
2993
- */
2994
- type LicenseStatus = "active" | "grace" | "expired" | "invalid" | "revoked" | "stale" | "none";
2995
- /** Decoded license token body (mirror of `@bivelio/protocol`'s LicensePayload,
2996
- * declared locally so the SDK stays free of a control-plane dependency). */
2997
- interface LicenseTokenPayload {
2998
- jti: string;
2999
- org_id: string;
3000
- plan: string;
3001
- seats: number;
3002
- features: string[];
3003
- issued_at: number;
3004
- expires_at: number;
3005
- grace_days: number;
3006
- iss?: string;
2924
+ serviceAccountKey?: string;
2925
+ /**
2926
+ * Automatic licence-renewal pickup (on by default when {@link serviceAccountKey}
2927
+ * and {@link licenseServerUrl} are both set).
2928
+ *
2929
+ * Turning it OFF means you take responsibility for deploying the renewed token
2930
+ * each billing period; if nobody does, the SDK stops at the end of the grace
2931
+ * period even though the subscription is paid.
2932
+ */
2933
+ licenseRefresh?: {
2934
+ enabled?: boolean;
2935
+ /** Check cadence in ms (default 3600000 = 1 h). */
2936
+ intervalMs?: number;
2937
+ /** How long before expiry to start looking, seconds (default 7 days). */
2938
+ leadSeconds?: number;
2939
+ };
2940
+ /**
2941
+ * Usage-ingest URL. Defaults to `${licenseServerUrl}/api/metering/usage`. Set
2942
+ * explicitly to point metering at a different host than the revocation poller.
2943
+ */
2944
+ meteringUrl?: string;
2945
+ /** Injectable `fetch` for metering (defaults to {@link licenseFetch}, then global). */
2946
+ meteringFetch?: typeof fetch;
2947
+ /** DETECTIVE metering tuning (all optional). */
2948
+ metering?: {
2949
+ /**
2950
+ * Off switch for USAGE REPORTING even when {@link serviceAccountKey} is set
2951
+ * (default: on). It does NOT disable licence-renewal pickup — that has its
2952
+ * own switch, {@link licenseRefresh}`.enabled`, because turning off telemetry
2953
+ * must not silently stop your licence from renewing.
2954
+ */
2955
+ enabled?: boolean;
2956
+ /** Flush cadence in ms (default 15000). */
2957
+ flushIntervalMs?: number;
2958
+ /** Records per POST (default 50). */
2959
+ maxBatch?: number;
2960
+ /** Hard in-memory cap; oldest records dropped past it (default 1000). */
2961
+ maxBuffer?: number;
2962
+ /** Override the auto-derived machine seat id. */
2963
+ seatId?: string;
2964
+ /** Override CI auto-detection (CI/ephemeral seats are excluded from billable seats). */
2965
+ ci?: boolean;
2966
+ /** Optional org hint; the server derives the authoritative org from the key. */
2967
+ orgId?: string;
2968
+ };
2969
+ /** Opt-in A/B verification on your own traffic. Off by default. See VerificationSettings. */
2970
+ verification?: VerificationSettings;
2971
+ /**
2972
+ * Development-only escape hatch to run ungated, honored ONLY when building from
2973
+ * source (local dev, CI, benchmarks) — also via the `BVSALA_DEV_OPEN=1`
2974
+ * environment variable.
2975
+ *
2976
+ * IMPORTANT: this flag is **disabled in the published npm package**. The
2977
+ * distributed build hard-codes the licensing check on, so setting
2978
+ * `devUnlicensed` (or `BVSALA_DEV_OPEN`) there has NO effect — the layer still
2979
+ * requires a valid `licenseKey`. It is not a way to run the product for free.
2980
+ */
2981
+ devUnlicensed?: boolean;
2982
+ /** Per-model cost models; falls back to `defaultCostModel`. */
2983
+ costModels?: Record<string, CostModel>;
2984
+ defaultCostModel?: CostModel;
2985
+ /** TTL for exact-cache writes, in ms. */
2986
+ exactCacheTtlMs?: number;
2987
+ /**
2988
+ * Refuse the single repair retry when paying for it would push this request's
2989
+ * cost above what it would have cost WITHOUT the layer. The invalid answer is
2990
+ * then returned as-is with `constraints.structurallyValid: false`, plus a
2991
+ * warning naming the three figures.
2992
+ *
2993
+ * OFF by default, and the default is a deliberate, measured choice rather than
2994
+ * caution — read this before turning it on.
2995
+ *
2996
+ * The bound sounds narrow ("only the worst case") and is not. The baseline of
2997
+ * a call that ran is reconstructed EXCLUDING the repair (see `estimateCost`:
2998
+ * `actualInput - repairInput`), precisely so a retry cannot pay for itself in
2999
+ * the report. That makes the threshold unreachable by construction: the repair
3000
+ * only ever moves `actual`, never the baseline, so ANY repair breaches it
3001
+ * whenever the layer's other books came out flat. On a request where nothing
3002
+ * was compressed, cached or eliminated, `baseline === actual` exactly and the
3003
+ * gate suppresses every repair, however cheap.
3004
+ *
3005
+ * So this is not "cap the tail", it is "stop repairing unless the layer has
3006
+ * already banked more than the repair will cost". That is a real quality
3007
+ * policy, not a rounding rule, and it is the caller's contract to decide on —
3008
+ * hence opt-in.
3009
+ *
3010
+ * There is no threshold that fires on the pathological case and spares the
3011
+ * flat one; they are the same arithmetic at different magnitudes. And there is
3012
+ * a genuine argument on the other side: an answer that misses its contract is
3013
+ * a MODEL failure, so the no-layer path would have had to pay for its own
3014
+ * second call too — charging the whole retry against the layer compares one
3015
+ * valid answer to one invalid answer.
3016
+ *
3017
+ * Turn it on when a hard per-request ceiling matters more than contract
3018
+ * compliance — a batch backfill, a cost-capped tenant. Leave it off when the
3019
+ * caller relies on the shape being right.
3020
+ */
3021
+ skipRepairAboveBaseline?: boolean;
3022
+ /**
3023
+ * Reliability contract. When true (the default), ANY internal optimizer error
3024
+ * (a resolver, cache, format selector, compressor, or accounting bug) is
3025
+ * caught and the request falls open to a plain provider call — BV-SALA must
3026
+ * never add a crash point to the customer's request path. Set to `false` only
3027
+ * in tests/benchmarks that want internal errors to surface.
3028
+ */
3029
+ failOpen?: boolean;
3030
+ /**
3031
+ * Silence BV-SALA's console warnings. They are ON by default, and they only
3032
+ * ever report a condition whose symptom is otherwise invisible: a saving knob
3033
+ * asked for but not wired up, usage that stops reaching the control plane, a
3034
+ * revocation list going stale. Nothing here throws and nothing here changes
3035
+ * what the layer does — turning it on only removes the console noise, so set
3036
+ * it when you have your own observability. Applies to the pipeline, the usage
3037
+ * emitter and the revocation poller alike.
3038
+ */
3039
+ silenceWarnings?: boolean;
3040
+ /**
3041
+ * Load the exact OpenAI tokenizer (`gpt-tokenizer`, optional peer) into the
3042
+ * default registry when it is installed. ON by default: without it, every
3043
+ * saving where the layer removed something is reported as `estimated` and
3044
+ * never billed, and the SDK warns once. Set to `false` to keep the built-in
3045
+ * heuristic even when the package is present (tests calibrated on the
3046
+ * heuristic, or a process that must not spend the encoding's memory). Ignored
3047
+ * when you pass your own `tokenizer` registry: you decide what it contains.
3048
+ */
3049
+ exactTokenizer?: boolean;
3050
+ /** Injectable clock for deterministic tests. */
3051
+ now?: () => number;
3052
+ /** Injectable id generator for deterministic tests. */
3053
+ generateId?: () => string;
3007
3054
  }
3008
- interface ResolvedLicense {
3009
- status: LicenseStatus;
3010
- payload?: LicenseTokenPayload;
3011
- reason?: string;
3055
+ /**
3056
+ * Tuning for the model cascade. Everything here is optional, and the defaults
3057
+ * cost nothing: with no settings at all, `modelCascade: "shadow"` measures what
3058
+ * it can measure for free and makes no extra provider call.
3059
+ */
3060
+ interface ModelCascadeSettings {
3061
+ /**
3062
+ * Your own strong → cheap pairs, replacing the built-in table.
3063
+ *
3064
+ * Keys and values are model ids WITHOUT a vendor prefix (`"gpt-4o"`, not
3065
+ * `"openai/gpt-4o"`); the caller's prefix is carried over automatically, and a
3066
+ * dated snapshot inherits from its base id. A pair whose economics do not work
3067
+ * out at your prices is refused with a reason in the report — declaring it here
3068
+ * does not override the arithmetic.
3069
+ */
3070
+ pairs?: Record<string, string>;
3071
+ /**
3072
+ * How many requests of a workload are routed before the automatic shut-off
3073
+ * judges the measured escalation rate (default 20).
3074
+ *
3075
+ * The rate is only observable on requests that were actually routed, so there
3076
+ * is no way to know without spending a bounded amount to find out. Worst case
3077
+ * (every one of them escalates) that exploration costs `n x r` requests'
3078
+ * worth of extra spend, once per workload per process — with a ratio of 0.06
3079
+ * and the default, about 1.2 requests.
3080
+ */
3081
+ exploration?: number;
3082
+ /**
3083
+ * PRICED OPT-IN, default 0 (off). Fraction of `"shadow"` requests on which the
3084
+ * layer also calls the cheap model, purely to measure.
3085
+ *
3086
+ * Shadow mode is free precisely because it does not run the cheap model: it
3087
+ * reports eligibility, the arithmetic, and what the swap would have been worth.
3088
+ * What it cannot know for free is the one number that decides everything — how
3089
+ * often the deterministic gate would have REJECTED the cheap answer. That
3090
+ * answer does not exist until the cheap model writes one.
3091
+ *
3092
+ * So this buys it, and the price is exact: each sampled request costs one extra
3093
+ * cheap call, i.e. `r` of what the request already cost — about 6% for
3094
+ * gpt-4o → gpt-4o-mini. At `0.05` that is roughly 0.3% on top of your bill, and
3095
+ * every sampled request reports its own `modelCascade.sampleCost`. It also
3096
+ * DOUBLES that request's provider latency, because the extra call is sequential.
3097
+ *
3098
+ * The sampled request's tokens count as spend and are held out of the baseline,
3099
+ * exactly like the repair retry: measurement must never pay for itself.
3100
+ *
3101
+ * Which requests get sampled is derived from the request's own cache key, so it
3102
+ * is deterministic and stable — the same request is always sampled or always
3103
+ * not, and no `Math.random` decides how your money is spent.
3104
+ */
3105
+ shadowSampleRate?: number;
3012
3106
  }
3013
- declare const EMBEDDED_PUBLIC_KEY: string;
3014
- /** Result of verifying a compact `head.body.sig` token's signature. */
3015
- interface CompactVerification {
3016
- /** True only if the signature is valid for the given public key. */
3017
- ok: boolean;
3018
- /** Decoded header (present when `ok`), e.g. `{ alg, typ, v }`. */
3019
- header?: Record<string, unknown>;
3020
- /** Raw payload bytes (present when `ok`); the caller decodes/parses them. */
3021
- payloadBytes?: Uint8Array;
3022
- reason?: string;
3107
+ /**
3108
+ * Defaults for the provider price levers. Everything here is off unless you set it,
3109
+ * and a request's own `savings` value always wins over it.
3110
+ */
3111
+ interface PriceLeverSettings {
3112
+ /** Default for `savings.deferrable` on every request of this layer. A request's own value wins. Defaults to false. */
3113
+ deferrable?: boolean;
3114
+ /** Default for `savings.standalone`. A request's own value wins. Defaults to false. */
3115
+ standalone?: boolean;
3116
+ /**
3117
+ * When a deferrable request finds no capacity on the cheaper tier (HTTP 429 or 503):
3118
+ * "throw" (default) surfaces the error unchanged; "standard" repeats it once at the
3119
+ * standard tier and price.
3120
+ */
3121
+ onCapacityError?: "throw" | "standard";
3023
3122
  }
3123
+ declare const DEFAULT_COST_MODEL: CostModel;
3124
+
3125
+ /**
3126
+ * Options shared by every framework adapter. Each one has the same meaning as in
3127
+ * `createSavingsLayer`; the adapter supplies the provider itself, which is the
3128
+ * framework's own model call, forwarded unchanged.
3129
+ */
3130
+ type SavingsAdapterOptions = Pick<SavingsLayerConfig, "licenseKey" | "licenseServerUrl" | "serviceAccountKey" | "licenseRefresh" | "licenseRevocation" | "licensePollIntervalSeconds" | "licenseFetch" | "metering" | "meteringUrl" | "meteringFetch" | "costModels" | "defaultCostModel" | "exactCache" | "exactCacheTtlMs" | "resolvers" | "exactTokenizer" | "silenceWarnings" | "failOpen"> & {
3131
+ /**
3132
+ * Tools without side effects, by name, or a function that says whether a tool
3133
+ * has none. Any other tool is treated as having side effects: a request that
3134
+ * offers it is never answered from a cache and never shares an answer with an
3135
+ * identical request in flight, because the action has to run.
3136
+ */
3137
+ readOnlyTools?: readonly string[] | ((toolName: string) => boolean);
3138
+ /**
3139
+ * Switch off, for every call, the levers that answer without calling the
3140
+ * model. All three are on by default.
3141
+ */
3142
+ savings?: SavingsAdapterLevers;
3143
+ /**
3144
+ * Called once per model call with its report. An error thrown here is caught
3145
+ * and never reaches the call.
3146
+ */
3147
+ onReport?: (report: SavingsReport) => void;
3148
+ };
3149
+ /**
3150
+ * The levers an adapter can use, all of them answers given without calling the
3151
+ * model: an exact repeat, an identical request already in flight, and a
3152
+ * computed answer (arithmetic). They can only be switched off.
3153
+ */
3154
+ type SavingsAdapterLevers = Pick<SavingsToggles, "exactCache" | "singleflight" | "deterministicResolvers">;
3155
+ /** Per-call options, where the framework has a place to pass them. */
3156
+ interface SavingsCallOptions {
3157
+ /** Cache identity for this call (tenant, session…). It wins over the adapter's `context` option. */
3158
+ context?: RequestContext;
3159
+ /** Switch off levers for this call only. */
3160
+ savings?: SavingsAdapterLevers;
3161
+ }
3162
+
3163
+ /**
3164
+ * Tokenizador EXACTO — la regla del proveedor, no una aproximación.
3165
+ *
3166
+ * Por qué existe. El baseline de una llamada que se ejecutó se reconstruye como
3167
+ * "lo enviado + lo quitado". Lo enviado lo mide el proveedor; lo quitado lo
3168
+ * contamos aquí. Con el estimador heurístico esos dos sumandos vienen de reglas
3169
+ * distintas, y la diferencia NO es ruido: el heurístico cobra cada símbolo suelto
3170
+ * como un token entero, así que infla el JSON (40% de símbolos) mucho más que el
3171
+ * An estimator that is not calibrated for the destination model can miss by
3172
+ * enough to publish a baseline several times the real one, which would credit a
3173
+ * saving that never existed. That is why `basis` degrades instead of claiming.
3174
+ */
3175
+
3176
+ /** Lo que necesitamos de gpt-tokenizer. Tipado aquí para no depender de sus tipos. */
3177
+ interface BpeEncoder {
3178
+ encode(text: string): number[] | {
3179
+ length: number;
3180
+ };
3181
+ }
3182
+ /**
3183
+ * Estimador exacto sobre una codificación BPE ya cargada.
3184
+ *
3185
+ * Se construye con el encoder ya resuelto en vez de importarlo dentro: así
3186
+ * este módulo no obliga a nadie a tener el paquete, y quien lo tiene decide qué
3187
+ * codificación carga (o200k_base para gpt-4o/4.1/5, cl100k_base para 3.5/4).
3188
+ */
3189
+ declare class ExactTokenizer implements TokenEstimator {
3190
+ private readonly encoder;
3191
+ readonly id: string;
3192
+ readonly exact = true;
3193
+ constructor(id: string, encoder: BpeEncoder);
3194
+ count(text: string): number;
3195
+ }
3196
+ /** Modelos que usan cada codificación. Sirve para mapear el registro de una vez. */
3197
+ declare const MODELS_BY_ENCODING: Readonly<Record<string, readonly string[]>>;
3198
+ /**
3199
+ * Carga `gpt-tokenizer` si está instalado y registra el tokenizador exacto para
3200
+ * los modelos de OpenAI. Devuelve `false` —sin lanzar— cuando no está: la
3201
+ * ausencia del paquete opcional no es un error, solo significa que la
3202
+ * contabilidad se publicará como estimada.
3203
+ *
3204
+ * Se importa solo la codificación pedida, no el paquete entero: `gpt-tokenizer`
3205
+ * expone un punto de entrada por codificación y cargarlas todas serían decenas
3206
+ * de MB en memoria sin motivo.
3207
+ */
3208
+ declare function registerExactTokenizer(registro: {
3209
+ register(e: TokenEstimator): unknown;
3210
+ mapModel(model: string, tokenizerId: string): unknown;
3211
+ }, opciones?: {
3212
+ codificaciones?: readonly string[];
3213
+ }): Promise<boolean>;
3214
+ /**
3215
+ * @deprecated Spanish-named aliases kept for one minor version. Use
3216
+ * {@link MODELS_BY_ENCODING} and {@link registerExactTokenizer}. The public
3217
+ * surface of this package is English-only; these leaked and are on the way out.
3218
+ */
3219
+ declare const MODELOS_POR_CODIFICACION: Readonly<Record<string, readonly string[]>>;
3220
+ /** @deprecated Use {@link registerExactTokenizer}. */
3221
+ declare const registrarTokenizadorExacto: typeof registerExactTokenizer;
3222
+
3223
+ /**
3224
+ * Exact-cache key.
3225
+ *
3226
+ * A hash of the user text alone is NOT a safe cache key. Two requests may share
3227
+ * text but differ in tenant, permission scope, model version, system prompt,
3228
+ * schema, toolset, locale, sampling, knowledge epoch or safety policy — any of
3229
+ * which can change the correct answer. The exact key includes all of them.
3230
+ *
3231
+ * `servedModel` CARRIES THE MODEL THAT WILL ACTUALLY ANSWER, not the one the
3232
+ * caller named, and that distinction is what stops the model cascade from
3233
+ * poisoning the cache. Routing to a cheap model and storing its answer under the
3234
+ * STRONG model's key would hand that answer to the next request — one with the
3235
+ * cascade off, or from another process entirely — while the report claims the
3236
+ * requested model served it. That is a silent model swap, deferred, which is the
3237
+ * worst kind of silent model swap.
3238
+ *
3239
+ * `modelFamily` is derived from whichever id is used, for the same reason: it is
3240
+ * part of the identity of whoever answers.
3241
+ */
3242
+ declare function buildExactKey(c: CanonicalRequest, opciones?: {
3243
+ servedModel?: string;
3244
+ }): string;
3245
+ /**
3246
+ * In-flight (singleflight) key. Coalesces concurrent, equivalent requests onto
3247
+ * a single execution. It can be slightly looser than the exact key (it does not
3248
+ * need freshness bucketing, since the work is happening *now*), but must still
3249
+ * isolate tenants and permissions.
3250
+ *
3251
+ * `schema` is REQUIRED here, even though `contract` is already part of the key:
3252
+ * two concurrent calls that differ only in `schemaVersion` are different
3253
+ * requests and must never coalesce. All three doors — exact key, in-flight key
3254
+ * and semantic filter — key on the same identity, by design.
3255
+ *
3256
+ * `servedModel` CARRIES THE MODEL THAT WILL ACTUALLY ANSWER, for the same reason
3257
+ * {@link buildExactKey} does, and this door is the one that leaks FIRST. The
3258
+ * exact key protects the answer AFTER it is stored; this one protects it while
3259
+ * it is still being written, and a follower does not re-run anything — it is
3260
+ * handed the leader's text verbatim.
3261
+ *
3262
+ * The leak it closes, measured: two concurrent requests for the same text, one
3263
+ * with `savings.modelCascade: "on"` and one with the cascade OFF, coalesced onto
3264
+ * one execution. The routed leader called the cheap model, and the caller who
3265
+ * never opted in — whose own decision was "do not route" — was served the cheap
3266
+ * model's answer. Same shape inside a single uniformly-configured deployment:
3267
+ * the automatic shut-off can latch between two concurrent requests, so one is
3268
+ * routed and the other is not, and coalescing hands one of them the other's
3269
+ * model. Whoever the layer decided must go to the strong model has to actually
3270
+ * go there.
3271
+ *
3272
+ * `deferrable` CLOSES THE SAME HOLE ON THE LATENCY AXIS, which is why it belongs
3273
+ * here and NOT in {@link buildExactKey}. The two doors do not risk the same
3274
+ * thing: the exact key hands out an answer that is ALREADY WRITTEN, where the
3275
+ * processing tier only picked a queue and changes neither the text nor the wait
3276
+ * of whoever receives it. This one binds a follower to a call that HAS NOT COME
3277
+ * BACK YET, so it imposes the leader's wait on them.
3278
+ *
3279
+ * A caller who never set `savings.deferrable` must not be made to wait for one
3280
+ * who did.
3281
+ */
3282
+ declare function buildInflightKey(c: CanonicalRequest, opciones?: {
3283
+ servedModel?: string;
3284
+ deferrable?: boolean;
3285
+ toolDiscovery?: boolean;
3286
+ maxOutputTokens?: number;
3287
+ }): string;
3288
+
3024
3289
  /**
3025
- * A pinned set of Ed25519 public keys addressed by key-id (`kid`), enabling signing
3026
- * key ROTATION without breaking already-deployed SDKs: new keys are added as extra
3027
- * map entries, and tokens carry the `kid` they were signed with. `defaultKid` is the
3028
- * key used for legacy tokens that carry NO `kid` (so today's single-key tokens keep
3029
- * verifying unchanged). Verification FAILS CLOSED on a `kid` not in the map.
3290
+ * Policy engine: risk and freshness.
3291
+ *
3292
+ * This is the gate that decides which savings are *safe* for a given request.
3293
+ * The most dangerous failure of a savings layer is not a cache miss — it is
3294
+ * confidently returning a wrong answer (a stale price, another tenant's data, a
3295
+ * reused answer for a side-effecting action). The policy engine exists to make
3296
+ * those outcomes structurally impossible, not merely unlikely.
3030
3297
  */
3031
- interface LicenseKeyset {
3032
- /** kid → SPKI PEM. */
3033
- keys: Record<string, string>;
3034
- /** kid used for tokens without a `kid` header (the current/legacy key). */
3035
- defaultKid: string;
3298
+ interface Policy {
3299
+ /** May the exact cache be read/written for this request? */
3300
+ allowExactCache: boolean;
3301
+ /** May the semantic cache even be *considered*? (still verified later) */
3302
+ allowSemanticCache: boolean;
3303
+ /** May concurrent equivalent requests be coalesced? */
3304
+ allowSingleflight: boolean;
3305
+ /** May we run natural-language compression on the context? */
3306
+ allowCompression: boolean;
3307
+ /**
3308
+ * May the SDK elide old, large tool_results before resending? Computed, not
3309
+ * hardcoded like `allowCompression`: eliding drops whole tool outputs, so it
3310
+ * honors the same vetoes as the semantic cache — write tools, freshness,
3311
+ * personalization, stochastic sampling and `day`.
3312
+ */
3313
+ allowElision: boolean;
3314
+ /** Does the request demand current/live data? */
3315
+ requiresFreshData: boolean;
3316
+ /** Does the request have (or request) side effects? */
3317
+ hasSideEffects: boolean;
3318
+ /** Is the response personalized to a user/account? */
3319
+ isPersonalized: boolean;
3320
+ /**
3321
+ * Is this agentic traffic? True when the request declares tools (read-only
3322
+ * ones included), carries tool calls or tool results, or holds more than one
3323
+ * user turn.
3324
+ *
3325
+ * Agentic traffic is kept out of the semantic cache by default, and only by
3326
+ * default: `savings.semanticCache: "include-agentic"` lets it in. Two runs of
3327
+ * an agent loop can differ only in what a tool returned, and the
3328
+ * deterministic guard compares figures, negation and word overlap, not
3329
+ * whether a tool said something else in words. The exact cache and elision
3330
+ * are not affected by this flag.
3331
+ */
3332
+ isAgentic: boolean;
3333
+ /**
3334
+ * Techo de antigüedad para una entrada de caché exacta, derivado del
3335
+ * `freshnessBucket`. `null` = sin techo propio (manda el TTL por defecto).
3336
+ * Existe porque el bucket NO particiona la clave —va crudo al hash y no rota—,
3337
+ * así que sin esto "hour" y "day" no significaban nada operativamente.
3338
+ */
3339
+ maxCacheAgeMs: number | null;
3340
+ reasons: string[];
3036
3341
  }
3037
- /** Narrow a verification key to a {@link LicenseKeyset} (vs a bare SPKI PEM string). */
3038
- declare function isKeyset(k: string | LicenseKeyset): k is LicenseKeyset;
3342
+ declare function evaluatePolicy(c: CanonicalRequest): Policy;
3343
+
3039
3344
  /**
3040
- * Normalize a license key the way it arrives from the real world — ONCE, at the
3041
- * entry point, never inside the verifier.
3345
+ * A LOCAL EMBEDDING FUNCTION — no network, no API key, no per-call cost.
3042
3346
  *
3043
- * The README says `export BIVELIO_LICENSE_KEY=…` and dotenv trims, so that path
3044
- * never showed it. But anyone reading the key from a file (`readFileSync`), from
3045
- * a Docker/Kubernetes secret (mounted as a file, trailing newline included) or
3046
- * copying it out of an `Authorization` header handed over a CORRECT key with a
3047
- * `\n` after it, a space before it, quotes around it or a `Bearer ` in front —
3048
- * and the SDK answered "does not verify, check you pasted the WHOLE key". A
3049
- * wrong diagnosis on a good key is the worst possible hint: it sends the
3050
- * customer to look where the problem is not.
3347
+ * The semantic cache needs a way to compare meaning. Wiring it to a hosted
3348
+ * embedding endpoint is the obvious answer and a bad default: it makes every
3349
+ * miss cost a network round-trip and money, and it forces a provider choice onto
3350
+ * a customer who never asked for one. So the cache shipped with no embedder at
3351
+ * all, which means it never matched anything.
3051
3352
  *
3052
- * Worse, it was INCONSISTENT: `tok + "\r\n"` passed and `tok + "\n"` did not,
3053
- * because `atob` ignores ASCII whitespace while the padding was computed from a
3054
- * length that included it, and two extra characters give the same remainder
3055
- * modulo 4 as zero. Behavior that depends on which editor saved the file is not
3056
- * behavior.
3353
+ * This is the third option: a deterministic bag-of-features vector computed in
3354
+ * microseconds from the text itself. It captures LEXICAL closeness — the same
3355
+ * question asked with different filler words — which is the overwhelming majority
3356
+ * of real repeat traffic. It does NOT capture deep semantics: "what is a CDN"
3357
+ * and "explain content delivery networks" share no tokens and will not match.
3057
3358
  *
3058
- * Only OUTER wrapping is removed — whitespace, one pair of single or double
3059
- * quotes, a `Bearer` prefix in any capitalization — in that order, in one pass.
3060
- * Anything INSIDE the key is left alone: an interior space is not wrapping, it
3061
- * is a broken key, and {@link licenseKeyShape} reports it as `malformed`.
3359
+ * WHY THAT IS SAFE, AND THIS IS THE PART THAT MATTERS. A semantic cache's one
3360
+ * unforgivable failure is confidently answering a NEARBY BUT DIFFERENT question.
3361
+ * This vector never gets the chance: the cache runs a deterministic precision
3362
+ * guard beneath the embedding — matching numbers and dates, negation parity and a
3363
+ * content-overlap floor — and every isolation field (tenant, principal, model,
3364
+ * schema, attached data) is a hard filter applied before similarity is scored.
3365
+ * A weak embedding can only cause a MISS, which costs one real call. It cannot
3366
+ * produce a wrong answer.
3367
+ *
3368
+ * Pass your own `embed` to {@link InMemorySemanticCache} to get true semantics.
3369
+ * This one exists so that the feature is worth something with zero setup.
3062
3370
  */
3063
- declare function normalizeLicenseKey(raw: string): string;
3064
3371
  /**
3065
- * The SHAPE of an already-normalized key, so the error message can tell apart
3066
- * three problems that used to get the same text:
3372
+ * The vector for a piece of text.
3067
3373
  *
3068
- * · `empty` — there is no key (only whitespace, or nothing at all).
3069
- * · `malformed` — not even the shape of a token: not three dot-separated
3070
- * base64url segments. This is where the `bvk_` service key
3071
- * pasted where the license goes lands, as does arbitrary text
3072
- * or a token with a line break in the middle.
3073
- * · `token` — has the shape; whether it verifies is up to the signature.
3374
+ * Features are unigrams plus adjacent bigrams: bigrams are what keep word ORDER
3375
+ * from being free. Without them "does A cause B" and "does B cause A" are the
3376
+ * same point in space.
3074
3377
  *
3075
- * It is the SAME shape rule {@link verifyCompactSignature} enforces, so that "has
3076
- * the shape" and "verification was attempted" mean the same thing.
3378
+ * Term weight is sublinear (`1 + log tf`) so a word repeated forty times does not
3379
+ * dominate the direction of the vector, and the result is L2-normalised so cosine
3380
+ * similarity is a plain dot product.
3077
3381
  */
3078
- type LicenseKeyShape = "empty" | "malformed" | "token";
3079
- /** Classify a normalized key. See {@link LicenseKeyShape}. */
3080
- declare function licenseKeyShape(normalizedKey: string): LicenseKeyShape;
3382
+ declare function vectorLocal(texto: string): number[];
3081
3383
  /**
3082
- * Verify the Ed25519 signature of any BV-SALA compact token
3083
- * (`base64url(header).base64url(body).base64url(sig)`, sig over `${head}.${body}`)
3084
- * and return the decoded header + raw payload bytes. This is the single shared
3085
- * crypto primitive: license tokens AND signed revocation lists both go through it,
3086
- * so the browser (`@noble/curves`) and node (`node:crypto`) sides agree byte-for-byte
3087
- * and there is no bespoke per-message verification path to keep in sync. It does NOT
3088
- * interpret the payload or the header `typ` — callers do that.
3384
+ * The default embedding function for {@link InMemorySemanticCache}.
3089
3385
  *
3090
- * Shape is checked SEGMENT by segment, not just by counting dots. Three parts
3091
- * used to be enough and the rest was left to `atob`, which ignores whitespace:
3092
- * that is how a token with `\r\n` after it verified while the same token with
3093
- * `\n` did not (see {@link normalizeLicenseKey}). The shared primitive must give
3094
- * the same answer to the same input, always; a caller that wants to tolerate
3095
- * wrapping normalizes BEFORE calling.
3386
+ * Synchronous by design — it returns the array, not a promise — so a cache lookup
3387
+ * adds no await and no latency to the request path.
3096
3388
  */
3097
- declare function verifyCompactSignature(token: string, publicKey: string | LicenseKeyset): CompactVerification;
3098
- /** Verify a token's signature and decode its payload. Does not check expiry. */
3099
- declare function verifyLicenseToken(token: string, publicKey: string | LicenseKeyset): {
3100
- signatureValid: boolean;
3101
- payload?: LicenseTokenPayload;
3102
- reason?: string;
3103
- };
3389
+ declare function embebedorLocal(texto: string): number[];
3104
3390
  /**
3105
- * Apply the grace + revocation policy to an already-decoded payload → a status.
3106
- * Pure and clock-injected, so a LIVE gate can call it on every request to pick up
3107
- * both revocation (jti in the set) AND expiry transitions without re-verifying the
3108
- * signature. Revocation takes precedence over the time-based status.
3109
- * jti on the revocation set → revoked
3110
- * now <= expires_at → active
3111
- * expires_at < now <= +grace_days → grace (still entitled)
3112
- * now > expires_at + grace_days → expired
3391
+ * The similarity floor this embedder is calibrated for.
3392
+ *
3393
+ * A lexical vector sits lower than a sentence-transformer on genuine paraphrases,
3394
+ * so the 0.9 default that suits a hosted embedder would reject almost everything
3395
+ * here. This value was measured on paraphrase pairs against near-miss pairs, not
3396
+ * guessed — see `test/embebedor-local.test.ts`, which fails if the two
3397
+ * populations stop being separable.
3113
3398
  */
3114
- declare function deriveStatus(payload: {
3115
- jti: string;
3116
- expires_at: number;
3117
- grace_days: number;
3118
- }, nowSeconds: number, revoked?: ReadonlySet<string>): LicenseStatus;
3399
+ declare const UMBRAL_LOCAL = 0.72;
3400
+
3119
3401
  /**
3120
- * Verify + apply the grace policy → a resolved status. Mirrors what the issuing
3121
- * control plane resolves for the same token. Bad signature / malformed
3122
- * → invalid; otherwise the status comes from {@link deriveStatus}.
3402
+ * Conversational memory by deltas.
3123
3403
  *
3124
- * A public entry point, so it normalizes like the others (gate and refresher):
3125
- * the same key must resolve the same whichever way it comes in.
3404
+ * A major source of wasted input tokens is resending the entire chat history
3405
+ * every turn. Instead we keep a compact structured state and, each turn, apply a
3406
+ * small delta produced BY THE MAIN GENERATION (not a separate summarization
3407
+ * call). The next prompt receives: compact relevant state + the last 1-3 raw
3408
+ * turns + the current message — never the whole conversation.
3126
3409
  */
3127
- declare function resolveLicense(token: string, publicKey: string | LicenseKeyset, opts?: {
3128
- nowSeconds?: number;
3129
- revoked?: ReadonlySet<string>;
3130
- }): ResolvedLicense;
3131
-
3132
- /** Decoded revocation-list body (mirror of @bivelio/protocol's RevocationListPayload). */
3133
- interface RevocationListBody {
3134
- v: number;
3135
- asOf: number;
3136
- seq: number;
3137
- revoked: string[];
3138
- iss?: string;
3410
+ interface ConversationState {
3411
+ goals: string[];
3412
+ confirmedFacts: Record<string, unknown>;
3413
+ userPreferences: Record<string, unknown>;
3414
+ decisions: string[];
3415
+ openItems: string[];
3416
+ evidenceRefs: string[];
3139
3417
  }
3140
- interface RevocationListVerification {
3141
- /** True only if signature + typ + monotonic-seq + skew all pass. */
3142
- ok: boolean;
3143
- /** List revision (monotonic). -1 when not ok. */
3144
- seq: number;
3145
- /** Unix seconds the list was minted. 0 when not ok. */
3146
- asOf: number;
3147
- /** Revoked jti in this list (empty when not ok). */
3148
- revoked: Set<string>;
3149
- reason?: string;
3418
+ interface MemoryDelta {
3419
+ set?: Record<string, unknown>;
3420
+ append?: Record<string, unknown[]>;
3421
+ remove?: string[];
3422
+ }
3423
+ declare function emptyState(): ConversationState;
3424
+ /**
3425
+ * Merge a delta into state locally. `set` updates confirmedFacts/preferences,
3426
+ * `append` pushes onto list fields, `remove` deletes fact keys or list entries.
3427
+ * The merge is deterministic and side-effect-free (returns a new object).
3428
+ */
3429
+ declare function mergeDelta(state: ConversationState, delta: MemoryDelta): ConversationState;
3430
+ /**
3431
+ * Project the state + recent turns into the minimal message set to send. Only
3432
+ * relevant, non-empty state is serialized, then the last `rawTurns` messages,
3433
+ * then the current message.
3434
+ */
3435
+ declare function projectContext(opts: {
3436
+ state: ConversationState;
3437
+ history: ChatMessage[];
3438
+ current: ChatMessage;
3439
+ rawTurns?: number;
3440
+ }): ChatMessage[];
3441
+ declare function renderState(state: ConversationState): string;
3442
+
3443
+ /**
3444
+ * Compact JSON: the lossless baseline every other format is measured against.
3445
+ * No indentation, no incidental whitespace. This is frequently the *winner* for
3446
+ * deeply nested or non-uniform data, which is exactly why it is the baseline and
3447
+ * The compact encoder is only ever a challenger.
3448
+ */
3449
+ declare const jsonCompactSerializer: Serializer;
3450
+
3451
+ /**
3452
+ * Compact row encoder.
3453
+ *
3454
+ * Its strength is UNIFORM ARRAYS OF FLAT OBJECTS: it declares the column
3455
+ * names once and then emits one row per object, which removes the repeated keys
3456
+ * that make JSON expensive for tabular data. On deeply nested or non-uniform
3457
+ * data it can be *larger* than compact JSON — so this is only ever a candidate
3458
+ * the selector may pick, never a default.
3459
+ *
3460
+ * This is an in-house, pinned encoder placed behind the {@link Serializer}
3461
+ * interface (as recommended: do not couple the system to an upstream `main`).
3462
+ * It emits a well-defined subset and ships a matching decoder so the JSON<->rows
3463
+ * round-trip is deterministic and testable.
3464
+ */
3465
+ interface CompactRowsOptions {
3466
+ /** Field delimiter within tabular/inline rows. */
3467
+ delimiter?: "," | "\t";
3468
+ /** Indentation unit. */
3469
+ indent?: string;
3150
3470
  }
3471
+ declare function encodeCompactRows(value: unknown, options?: CompactRowsOptions): string;
3472
+ /** Return shared column names iff every element is a flat, scalar-valued object with identical keys. */
3473
+ declare function uniformColumns(arr: unknown[]): string[] | null;
3151
3474
  /**
3152
- * Verify a signed revocation-list token offline and enforce the client-side
3153
- * policy: (1) valid Ed25519 signature, (2) header `typ` is a revocation list
3154
- * (a license token can't be replayed here), (3) `seq >= minSeq` — reject a
3155
- * stale/rolled-back list, (4) `asOf <= now + maxSkew` — reject a future-dated list.
3156
- * ANY failure returns `ok:false` with an empty set; the poller then keeps its
3157
- * sticky last-known-good (fail-open), never dropping known revocations.
3475
+ * The instruction a model needs to read the compact encoder. Kept short on purpose — this text
3476
+ * is the per-request overhead the selector must amortize before it wins.
3158
3477
  */
3159
- declare function verifyRevocationList(token: string, publicKey: string | LicenseKeyset, opts?: {
3160
- minSeq?: number;
3161
- nowSeconds?: number;
3162
- maxSkewSeconds?: number;
3163
- }): RevocationListVerification;
3478
+ declare const COMPACT_ROWS_INSTRUCTION: string;
3479
+ declare function createCompactRowsSerializer(options?: CompactRowsOptions): Serializer;
3480
+ declare const compactRowsSerializer: Serializer;
3481
+
3164
3482
  /**
3165
- * The SDK's live, sticky, monotonic view of the revocation set. It only ever
3166
- * GROWS (a jti, once seen revoked, stays revoked for the process lifetime) and
3167
- * only advances on a higher-or-equal `seq` — so a stale, rolled-back, or empty
3168
- * list can never un-revoke a license. The live gate reads {@link snapshot} on
3169
- * every request; the poller feeds it via {@link apply}.
3483
+ * Decoder for the subset emitted by {@link encodeCompactRows}.
3484
+ *
3485
+ * Supports: scalar fields, nested objects, empty arrays, inline scalar arrays
3486
+ * (`key[N]: a,b,c`) and tabular object arrays (`key[N]{c1,c2}:` + rows). This
3487
+ * covers the cases the format selector actually chooses it for, and makes the
3488
+ * JSON<->rows round-trip deterministic and testable. List-form arrays of nested
3489
+ * objects are intentionally out of scope for decoding (the selector never picks
3490
+ * it for those, since JSON wins there anyway).
3170
3491
  */
3171
- declare class RevocationSnapshot {
3172
- private _seq;
3173
- private _asOf;
3174
- private _lastContactAt;
3175
- private readonly set;
3176
- get seq(): number;
3177
- get asOf(): number;
3178
- /** Unix seconds of the last CONFIRMED authorized heartbeat (a 304 or a verified
3179
- * list). Feeds the live gate's online staleness check; 0 until the first one. */
3180
- get lastContactAt(): number;
3181
- /** Record a confirmed authorized heartbeat from the control plane (monotonic). */
3182
- markContact(nowSeconds: number): void;
3183
- has(jti: string): boolean;
3184
- /** Live view of the revoked set (the same Set instance — read-only to callers). */
3185
- snapshot(): ReadonlySet<string>;
3186
- /**
3187
- * Merge an already-verified list. Accepts only `seq >= current seq`; UNIONS the
3188
- * jti (never removes); advances seq/asOf monotonically. Returns whether the
3189
- * revoked set grew or the revision advanced.
3190
- */
3191
- apply(list: {
3192
- seq: number;
3193
- asOf: number;
3194
- revoked: Iterable<string>;
3195
- }): boolean;
3492
+ interface CompactRowsDecodeOptions {
3493
+ delimiter?: "," | "\t";
3494
+ indent?: string;
3196
3495
  }
3496
+ declare function decodeCompactRows(text: string, options?: CompactRowsDecodeOptions): unknown;
3197
3497
 
3198
- /** A resolved gate that also exposes overall status + a refresh horizon. */
3199
- interface ResolvedGate extends LicenseGate {
3200
- isActive(): boolean;
3201
- status(): LicenseStatus;
3202
- /**
3203
- * Why the key does not open, when it does not; `undefined` when it verified.
3204
- * Under `status() === "invalid"` it tells a key that does NOT EVEN HAVE THE
3205
- * SHAPE of a token (`"malformed"`: the `bvk_` pasted where the license goes,
3206
- * arbitrary text) from a well-formed one whose signature fails (the verifier's
3207
- * reason, e.g. `"bad signature"`). Both are `invalid` — the stable handle for
3208
- * branching does not change — but the advice given to the customer differs.
3209
- */
3210
- reason(): string | undefined;
3211
- /** Unix seconds after which the online status should be re-checked (poller anchor). */
3212
- refreshAfter?(): number | undefined;
3213
- }
3214
3498
  /**
3215
- * Build a LIVE license gate from a signed key. The token is verified + decoded
3216
- * ONCE (no per-request Ed25519 verify — hot-path safe); each `isActive()` /
3217
- * `status()` / `isEntitled()` then re-derives the status from the cached payload +
3218
- * the LIVE clock + the (mutable, sticky) {@link RevocationSnapshot}. This is what
3219
- * lets a revocation — or an expiry crossed mid-process — take effect without a
3220
- * restart, while the customer request path never touches the network (it reads an
3221
- * in-memory Set).
3222
- *
3223
- * The offline path passes a fresh, never-updated snapshot (live clock, zero
3224
- * network); the online path passes a snapshot a {@link RevocationPoller} feeds.
3499
+ * CSV serializer — only valid for genuinely tabular data (a uniform array of
3500
+ * flat objects). CSV can be marginally smaller than the compact rows on pure tables, but it
3501
+ * does NOT preserve the JSON data model in general, so `supports()` is strict.
3225
3502
  */
3226
- declare function liveGateFromKey(licenseKey: string, publicKey: string | LicenseKeyset, opts: {
3227
- revocation: RevocationSnapshot;
3228
- now?: () => number;
3229
- /** Online staleness ceiling, seconds. If the gate goes this long without a
3230
- * confirmed authorized heartbeat, it stops granting (closes the block-egress
3231
- * fail-open). Undefined = check disabled (the offline path never sets it). */
3232
- maxStalenessSeconds?: number;
3233
- /** Freshness anchor (unix seconds); defaults to construction time (boot). */
3234
- startedAt?: number;
3235
- }): ResolvedGate;
3503
+ declare const csvSerializer: Serializer;
3236
3504
 
3237
3505
  /**
3238
- * Build the SDK's structural {@link import("../contracts/types.js").LicenseGate}
3239
- * from a signed license key — the object `createSavingsLayer({ licenseKey })`
3240
- * injects on the OFFLINE path (no `licenseServerUrl`).
3506
+ * Structural validation against the JsonSchema subset the layer uses.
3241
3507
  *
3242
- * It is a LIVE gate over an EMPTY, never-updated revocation snapshot: zero network,
3243
- * but the clock is LIVE, so a license that crosses its expiry (past grace) stops
3244
- * working mid-process instead of only after a restart — closing the frozen-clock
3245
- * hole the old one-shot resolution left open. `isActive()` is true while active or
3246
- * in grace; `isEntitled` additionally requires the feature in the token's grant.
3247
- * Online revocation is added by pairing {@link liveGateFromKey} with a
3248
- * poller-fed snapshot, which `createSavingsLayer` wires up for you.
3508
+ * Constrained decoding should prevent invalid structures at generation time,
3509
+ * but a provider without native support can still return free text. This
3510
+ * validator is the deterministic gate that decides whether a response satisfies
3511
+ * its output contract — and, when it does not, whether a single repair is
3512
+ * warranted. NOTE: structural validity is NOT semantic validity; risky tasks
3513
+ * still need a semantic check on top.
3249
3514
  */
3250
- declare function gateFromKey(licenseKey: string, publicKey: string | LicenseKeyset, now?: () => number): ResolvedGate;
3251
-
3252
- type RevocationPollEvent = {
3253
- type: "applied";
3254
- seq: number;
3255
- size: number;
3256
- } | {
3257
- type: "unchanged";
3258
- seq: number;
3259
- } | {
3260
- type: "rejected";
3261
- reason: string;
3262
- } | {
3263
- type: "error";
3264
- error: unknown;
3265
- } | {
3266
- type: "no-fetch";
3267
- };
3268
- interface RevocationPollerOptions {
3269
- /** Absolute URL of GET /licenses/revocations. */
3270
- url: string;
3271
- /** SPKI PEM (or a kid-addressed {@link LicenseKeyset}) used to verify the signed
3272
- * list — the same key/keyset that verifies license tokens. */
3273
- publicKeyPem: string | LicenseKeyset;
3274
- /** The sticky snapshot to feed. */
3275
- snapshot: RevocationSnapshot;
3276
- /** Anchor cadence to the license refresh horizon (unix seconds). */
3277
- getRefreshAfter?: () => number | undefined;
3278
- /** Injectable clock (unix seconds). */
3279
- nowSeconds?: () => number;
3280
- /** Injectable fetch (defaults to the global). */
3281
- fetchImpl?: typeof fetch;
3282
- pollIntervalSeconds?: number;
3283
- floorSeconds?: number;
3284
- maxIntervalSeconds?: number;
3285
- jitterRatio?: number;
3286
- maxSkewSeconds?: number;
3287
- requestTimeoutMs?: number;
3288
- /** Injectable RNG in [0,1) for jitter. Default Math.random. */
3289
- random?: () => number;
3290
- /** Optional observer (telemetry/tests). */
3291
- onEvent?: (e: RevocationPollEvent) => void;
3292
- /**
3293
- * Silencia los avisos por consola. Por defecto están ENCENDIDOS, y ese defecto
3294
- * es la corrección: el sondeo es fail-open, así que un call-home roto no rompe
3295
- * nada visible — la lista de revocaciones simplemente se queda vieja, en
3296
- * silencio, durante días. Quien tenga observabilidad propia puede apagarlos;
3297
- * quien no la tenga, que es casi todo el mundo, se entera igual.
3298
- */
3299
- silenceWarnings?: boolean;
3300
- /**
3301
- * @deprecated Spanish name that leaked into the public options. Use
3302
- * {@link RevocationPollerOptions.silenceWarnings}. Honoured for one minor
3303
- * version so nobody gets their warnings back by surprise.
3304
- */
3305
- silenciarAvisos?: boolean;
3515
+ interface ValidationError {
3516
+ path: string;
3517
+ message: string;
3306
3518
  }
3519
+ interface ValidationResult {
3520
+ valid: boolean;
3521
+ errors: ValidationError[];
3522
+ }
3523
+ declare function validateAgainstSchema(value: unknown, schema: JsonSchema, path?: string): ValidationResult;
3524
+
3307
3525
  /**
3308
- * PURE, unit-testable next-delay policy (seconds). Steady state anchors to the
3309
- * license refresh horizon, clamped to [floor, ceiling] and jittered. After a
3310
- * failure it backs off exponentially (a revocation can't land during an outage
3311
- * anyway), capped, still jittered, never below the floor.
3312
- */
3313
- declare function computeNextDelay(opts: {
3314
- refreshAfter?: number;
3315
- nowSeconds: number;
3316
- consecutiveFailures: number;
3317
- floorSeconds: number;
3318
- pollIntervalSeconds: number;
3319
- maxIntervalSeconds: number;
3320
- jitterRatio: number;
3321
- random?: () => number;
3322
- }): number;
3323
- declare class RevocationPoller {
3324
- private readonly opts;
3325
- private timer;
3326
- private controller;
3327
- private consecutiveFailures;
3328
- private stopped;
3329
- private started;
3330
- private warnedNoFetch;
3331
- /** ETag of the last 200; sent as If-None-Match so an unchanged list 304s (cheap no-op). */
3332
- private lastEtag;
3333
- /** 304 seguidos; al llegar al tope se fuerza una descarga firmada. */
3334
- private consecutive304;
3335
- constructor(opts: RevocationPollerOptions);
3336
- private now;
3337
- private resolveFetch;
3338
- /**
3339
- * One poll cycle: fetch → verify → apply. NEVER throws (fail-open). A reachable
3340
- * CP resets the failure backoff even if the returned list is rejected — we keep
3341
- * last-known-good. Only a transport/abort/parse error counts as a failure.
3342
- */
3343
- pollOnce(): Promise<void>;
3344
- /** El interruptor, aceptando todavía el nombre viejo. */
3345
- private callado;
3346
- /** Un aviso por causa y por proceso, respetando el interruptor. */
3347
- private avisar;
3526
+ * Deterministic mock provider.
3527
+ *
3528
+ * Lets the whole layer run — and be tested — with no network and no
3529
+ * credentials. When a schema is supplied it fabricates a schema-valid object,
3530
+ * demonstrating the constrained-decoding contract end to end. Token usage is
3531
+ * derived from the heuristic estimator so SavingsReport numbers are meaningful
3532
+ * in examples and tests.
3533
+ *
3534
+ * ⚠️ WHAT THIS PROVIDER CANNOT MEASURE, and it has already cost money once.
3535
+ * It fabricates the answer FROM THE SCHEMA it receives, so a smaller schema
3536
+ * always produces a smaller answer. Any lever that shrinks the contract sent to
3537
+ * the provider therefore appears to shrink the OUTPUT too — automatically, on
3538
+ * every run, whatever model id is being simulated. That is a property of this
3539
+ * file, not of any model.
3540
+ *
3541
+ * The output codebook is exactly that shape of lever, and against this provider
3542
+ * the benchmark reported it saving 24.8%. Measured live it saves 8.0% on
3543
+ * `gpt-4o` and COSTS 8.2% on `gpt-4o-mini`, because a real model decides how
3544
+ * much to WRITE and a smaller model writes more when the field names carry less
3545
+ * meaning. So: never take an output-token saving measured here as evidence of
3546
+ * anything. Prove those with the paired A/B in `benchmarks/ab-live.ts`, and
3547
+ * write tests for output-side effects against a provider that models the
3548
+ * behaviour you are claiming — see `test/codebook-de-salida.test.ts`.
3549
+ */
3550
+ interface MockProviderOptions {
3551
+ /** Fixed text to return when no schema is present. */
3552
+ answer?: string;
3553
+ /** Override the fabricated structured object. */
3554
+ data?: unknown;
3555
+ /** Simulated provider-cached input tokens (ledger 4). */
3556
+ cachedInputTokens?: number;
3557
+ /** Characters per text event in generateStream (default 8). */
3558
+ streamChunkChars?: number;
3559
+ }
3560
+ declare class MockProvider implements Provider {
3561
+ readonly id = "mock";
3562
+ private readonly options;
3563
+ constructor(options?: MockProviderOptions);
3564
+ generate(req: ProviderRequest, _options?: ProviderStreamOptions): Promise<ProviderResponse>;
3348
3565
  /**
3349
- * THE CONTROL PLANE HAS BEEN UNREACHABLE FOR A WHILE.
3350
- *
3351
- * Polling is fail-open by design: a network outage must never take down the
3352
- * customer's production traffic. The trade-off is that an unreachable control
3353
- * plane has no visible symptom — the license keeps working and requests keep
3354
- * flowing while the revocation list quietly goes stale.
3355
- *
3356
- * So the warning goes to the console BY DEFAULT, not only through the optional
3357
- * `onEvent` observer. It fires on the third consecutive failure and then every
3358
- * twelfth: a lost request is normal, a week-long outage must keep reminding.
3359
- * Silence it with `silenceWarnings` if you have your own observability.
3566
+ * Streaming variant of generate(): the same answer, cut into text events of
3567
+ * `streamChunkChars` characters, then one `done` with the generate() response.
3360
3568
  */
3361
- private avisarFalloDeRed;
3362
- /** Fire one non-blocking warm poll, then self-schedule. Idempotent. */
3363
- start(): void;
3364
- private schedule;
3365
- /** Stop polling and abort any in-flight request. Idempotent. */
3366
- stop(): void;
3569
+ generateStream(req: ProviderRequest): AsyncGenerator<ProviderStreamEvent, void, undefined>;
3367
3570
  }
3571
+ declare function createMockProvider(options?: MockProviderOptions): Provider;
3368
3572
 
3369
- interface UsageEmitterOptions {
3370
- /** Full ingest URL, e.g. `https://savings.bivelio.com/api/metering/usage`. */
3371
- url: string;
3372
- /** Service-account key (`bvk_…`) sent as a Bearer token; the server derives the org from it. */
3373
- serviceAccountKey: string;
3374
- /** Optional org hint; the server always derives the authoritative org from the key. */
3375
- orgId?: string;
3376
- /** Flush cadence in ms (default 15000). */
3377
- flushIntervalMs?: number;
3378
- /** Records per POST (default 50). */
3379
- maxBatch?: number;
3380
- /** Hard in-memory cap; oldest records are dropped past it (default 1000). */
3381
- maxBuffer?: number;
3382
- /** Injectable fetch (defaults to the global). */
3383
- fetchImpl?: typeof fetch;
3384
- /** Injectable clock. */
3385
- now?: () => number;
3386
- /** Whether this process is a CI/ephemeral runner (excluded from billable seats). */
3387
- ci?: boolean;
3388
- /**
3389
- * Which surface owns this process: the local gateway (`bvsala claude` and
3390
- * friends) or the in-process SDK. Sent with the version in the batch's
3391
- * `x-bvsala-version` header (`gateway/0.4.16`, `sdk/0.4.16`) so the control
3392
- * plane can tell a gateway that lacks a lever from an SDK that never had it.
3393
- * Defaults to `"sdk"`; the gateway sets `"gateway"`.
3394
- */
3395
- surface?: "gateway" | "sdk";
3573
+ /**
3574
+ * OpenAI-compatible provider adapter (chat/completions shape).
3575
+ *
3576
+ * Works with any endpoint that speaks the OpenAI Chat Completions API — OpenAI,
3577
+ * Azure, vLLM, SGLang, TGI, a local gateway, or Vercel AI Gateway with a
3578
+ * "provider/model" string. Structured output is requested via `response_format`
3579
+ * with a JSON schema when a contract is present, which is the provider-native
3580
+ * constrained-decoding path preferred by the layer.
3581
+ *
3582
+ * Models that predate structured outputs reject `response_format` with a 400. The
3583
+ * adapter degrades for those: it repeats the request once with the schema carried
3584
+ * in the prompt instead, remembers the model so later requests skip the rejected
3585
+ * attempt, and reports `structuredNatively: false` so the savings report states
3586
+ * `constraints.backend: "none"` rather than claiming a guarantee the provider
3587
+ * never gave.
3588
+ *
3589
+ * Network calls use global `fetch` (Node >= 18) so there is no SDK dependency.
3590
+ */
3591
+ interface OpenAICompatibleOptions {
3592
+ baseUrl: string;
3593
+ apiKey?: string;
3396
3594
  /**
3397
- * Se llama cuando el ingest rechaza la credencial (401/403). Existe para que un
3398
- * despliegue con observabilidad pueda ENTERARSE: el aviso por consola sirve en
3399
- * un portátil, pero en producción los `console.warn` no despiertan a nadie, y
3400
- * este fallo concreto es invisible por definición —deja de llegar uso, que es
3401
- * exactamente lo mismo que se ve cuando no hay tráfico—.
3595
+ * Strip the `provider/` prefix from model ids before sending (`openai/gpt-4o-mini`
3596
+ * → `gpt-4o-mini`).
3597
+ *
3598
+ * Default (undefined): strip when `baseUrl` is OpenAI's own API or an Azure
3599
+ * OpenAI endpoint, keep otherwise. OpenAI rejects a prefixed id with a 404
3600
+ * ("model does not exist"), so the qualified ids the layer uses everywhere
3601
+ * else — pricing, cache keys, the report — would break the very first call
3602
+ * that reaches the provider. A gateway or a self-hosted server with a
3603
+ * namespaced model name usually WANTS the prefix, so it is kept there. Set `true`/`false` to decide explicitly; an explicit value
3604
+ * always wins.
3402
3605
  */
3403
- onAuthError?: (status: number) => void;
3606
+ stripProviderPrefix?: boolean;
3607
+ /** Extra headers (e.g. for a gateway). */
3608
+ headers?: Record<string, string>;
3609
+ /** Injectable fetch for testing. */
3610
+ fetchImpl?: typeof fetch;
3404
3611
  /**
3405
- * Silence the console warnings. They are ON by default, exactly as in the
3406
- * revocation poller: metering is best-effort, so a broken ingest breaks
3407
- * nothing visible — usage simply stops arriving, silently, and the customer's
3408
- * dashboard stays empty forever.
3612
+ * When true (recommended for measurement), a response whose `usage` block is
3613
+ * missing or reports zero prompt/completion tokens for a call that actually
3614
+ * ran is treated as a HARD ERROR instead of being silently coerced to
3615
+ * 0 tokens. This closes the single most dishonest failure mode: an absent
3616
+ * usage payload otherwise becomes `actual cost = 0` → a fabricated 100%
3617
+ * saving. Benchmarks that make real cost claims must set this.
3409
3618
  */
3410
- silenceWarnings?: boolean;
3619
+ requireUsage?: boolean;
3411
3620
  }
3412
3621
  /**
3413
- * Buffers counts-only usage and flushes it to the control plane. DETECTIVE
3414
- * metering: strictly best-effort and NON-BLOCKING. `record()` is O(1) and
3415
- * `flush()` never throws, so a slow, unreachable, or erroring control plane can
3416
- * neither block nor crash the customer's request path. The buffer is hard-capped
3417
- * so a sustained outage can never grow memory without bound.
3622
+ * ¿Cumple este esquema las reglas del modo `strict` de OpenAI?
3623
+ *
3624
+ * Son dos, y aplican a TODO objeto anidado:
3625
+ * 1. `additionalProperties: false`.
3626
+ * 2. `required` tiene que listar TODAS las claves de `properties` — no solo
3627
+ * las obligatorias. En modo strict no existe el campo opcional.
3628
+ *
3629
+ * Un esquema perfectamente válido como
3630
+ * { properties: { answer, refs }, required: ["answer"] }
3631
+ * incumple la segunda y el proveedor responde 400. No es un fallo del cliente:
3632
+ * es una restricción de OpenAI que JSON Schema no tiene.
3418
3633
  */
3419
- declare class UsageEmitter {
3420
- private readonly options;
3421
- private readonly now;
3422
- private readonly flushIntervalMs;
3423
- private readonly maxBatch;
3424
- private readonly maxBuffer;
3425
- private buffer;
3426
- private seatId;
3427
- private ci;
3428
- private timer;
3429
- private inflight;
3430
- /**
3431
- * CONSECUTIVE failed flushes. Warn on the third and then every twelfth: one
3432
- * lost batch is normal, a whole hour is not. Same cadence as the revocation
3433
- * poller on purpose — two different rhythms for the same class of failure
3434
- * only confuse whoever reads the logs.
3435
- */
3436
- private fallosSeguidos;
3437
- constructor(options: UsageEmitterOptions);
3438
- /** Set the resolved seat identity (derivation is async; records emitted before
3439
- * it resolves are stamped at flush time, so none are lost). */
3440
- setSeat(seatId: string | undefined, ci: boolean): void;
3441
- /** Record one request's usage. Never blocks; O(1). */
3442
- record(report: SavingsReport): void;
3443
- start(): void;
3444
- /** Stop the timer and attempt a final flush. Safe to call more than once.
3445
- * Fire-and-forget: for a CLEAN shutdown that must not lose the last batch,
3446
- * use `stopAndDrain` and await it. */
3447
- stop(): void;
3448
- /** Clean shutdown: stop the timer and DRAIN the buffer, awaiting each POST.
3449
- * Unlike `stop()` (fire-and-forget), this resolves only once the final batch
3450
- * has actually been sent, so a graceful exit doesn't lose the last session's
3451
- * usage. Bounded, so an unreachable control plane can't block shutdown. */
3452
- stopAndDrain(maxIntentos?: number): Promise<void>;
3453
- /**
3454
- * Flush one batch. Never throws. A transient server error (5xx) or network
3455
- * failure re-queues the batch (capped); a 4xx (bad request / bad key) drops it,
3456
- * since retrying a rejected payload would loop forever.
3457
- */
3458
- flush(): Promise<void>;
3459
- private hacerFlush;
3634
+ declare function isStrictCompatible(schema: unknown): boolean;
3635
+ /**
3636
+ * Decides the id that travels on the wire. Explicit option wins; otherwise the
3637
+ * endpoint decides: OpenAI/Azure get the bare id, everything else keeps what
3638
+ * the caller wrote. Exported for the pipeline-level test that pins the README
3639
+ * snippet; not part of the public API.
3640
+ */
3641
+ declare function quitarPrefijoDeProveedor(model: string, explicito: boolean | undefined, baseUrl: string): string;
3642
+ declare class OpenAICompatibleProvider implements Provider {
3643
+ readonly id: string;
3644
+ private readonly opts;
3645
+ private readonly fetchImpl;
3646
+ /** Models of this endpoint already known to reject `response_format`. */
3647
+ private readonly sinResponseFormat;
3648
+ private streamOptionsRejected;
3649
+ private readonly origen;
3650
+ constructor(opts: OpenAICompatibleOptions);
3651
+ generate(req: ProviderRequest, options?: ProviderStreamOptions): Promise<ProviderResponse>;
3460
3652
  /**
3461
- * THE INGEST HAS BEEN FAILING FOR A WHILE AND NOBODY NOTICED.
3462
- *
3463
- * Metering is best-effort by design — it must never take down the customer's
3464
- * request — and that turns every failure into SILENT data loss. Without this,
3465
- * the only symptom is an empty dashboard, which is indistinguishable from
3466
- * "you haven't sent anything yet": the symptom points the opposite way from
3467
- * the cause.
3468
- *
3469
- * Warns on the third consecutive failure and then every twelfth. Not the
3470
- * first — a lost request is normal — and not every one, which becomes noise.
3653
+ * Streaming variant of generate() over Chat Completions. It always asks for
3654
+ * the final usage chunk (`stream_options.include_usage`); an endpoint that
3655
+ * rejects that option is retried once without it and remembered, and its
3656
+ * streamed answers then report `usage.complete: false`. With `requireUsage`,
3657
+ * that rejection throws instead. Requests with an output contract or a
3658
+ * deferrable tier are not streamed: they are served by generate() and yielded
3659
+ * as one text event.
3471
3660
  */
3472
- private avisarDeFallo;
3473
- private requeue;
3474
- private toWire;
3661
+ generateStream(req: ProviderRequest, options?: ProviderStreamOptions): AsyncGenerator<ProviderStreamEvent, void, undefined>;
3662
+ private enviar;
3663
+ /** Translate a 2xx endpoint response into the provider-neutral shape. */
3664
+ private leer;
3475
3665
  }
3476
3666
 
3477
- /** True when running in a CI / ephemeral environment. */
3478
- declare function detectCi(): boolean;
3479
- /**
3480
- * Derive the machine seat id for a service-account key. Resolves to `undefined`
3481
- * when no machine fingerprint is available (e.g. a browser) — the caller then
3482
- * reports no seatId and the server counts the request under an "unknown" seat.
3483
- * The id (`m-<32 hex>`) matches the control plane's identity charset.
3484
- */
3485
- declare function deriveSeatId(serviceAccountKey: string): Promise<string | undefined>;
3486
-
3487
3667
  /**
3488
- * Everything the pipeline depends on. All fields except `provider` and
3489
- * `tokenizer` are optional; sensible in-memory defaults are wired by
3490
- * {@link createSavingsLayer} so the layer works out of the box and every
3491
- * component is independently swappable.
3668
+ * Anthropic Messages API adapter.
3669
+ *
3670
+ * Speaks `POST /v1/messages`: the system prompt travels in its own top-level
3671
+ * `system` field (not as a message), tools are `{name, description,
3672
+ * input_schema}` with no `function` wrapper, and prefix caching is requested
3673
+ * with EXPLICIT `cache_control` breakpoints — Anthropic does not cache on its
3674
+ * own. A cached read is billed at 0.1x the input rate, which is the largest
3675
+ * single lever on the input side of a customer's bill.
3676
+ *
3677
+ * Network calls use global `fetch` (Node >= 18) so there is no SDK dependency.
3492
3678
  */
3493
- interface SavingsLayerConfig {
3679
+ /** Render order of the request, which is also the order the cache prefix grows in. */
3680
+ interface AnthropicOptions {
3681
+ apiKey?: string;
3682
+ /** Defaults to the public API. Override for a gateway or a proxy. */
3683
+ baseUrl?: string;
3684
+ /**
3685
+ * Strip the `provider/` prefix from model ids before sending
3686
+ * (`anthropic/claude-sonnet-4-5` → `claude-sonnet-4-5`). Default (undefined):
3687
+ * strip when talking to `api.anthropic.com`, which rejects a prefixed id;
3688
+ * keep for a gateway or proxy, which usually routes on it. An explicit value
3689
+ * always wins.
3690
+ */
3691
+ stripProviderPrefix?: boolean;
3692
+ /** Wire version pinned in the `anthropic-version` header. */
3693
+ apiVersion?: string;
3694
+ /** Extra headers (e.g. for a gateway). */
3695
+ headers?: Record<string, string>;
3696
+ /** Injectable fetch for testing. */
3697
+ fetchImpl?: typeof fetch;
3494
3698
  /**
3495
- * Contratos a los que ya se les incumplió una restricción de valor.
3496
- *
3497
- * La capa lo aprende sola, pero lo aprendido muere con la instancia. En
3498
- * serverless —una instancia por petición— eso significa no aprender NUNCA:
3499
- * cada llamada estrena y paga su reparación. Pasar aquí un conjunto
3500
- * compartido (o respaldado por Redis, con la API de Set) hace que se aprenda
3501
- * una vez para todo el despliegue.
3502
- *
3503
- * Solo guarda identificadores de contrato, nunca contenido de peticiones.
3699
+ * `max_tokens` is REQUIRED by this API — unlike the OpenAI shape, where it is
3700
+ * optional. A request without one is a 400, so the adapter has to supply a
3701
+ * value. It is deliberately generous: a low default would truncate answers
3702
+ * and the truncation would look like a model failure rather than an adapter
3703
+ * default.
3504
3704
  */
3505
- constraintMemory?: Set<string>;
3506
- provider: Provider;
3507
- tokenizer: TokenizerRegistry;
3508
- exactCache?: ExactCache;
3509
- semanticCache?: InMemorySemanticCache;
3510
- singleflight?: Singleflight;
3511
- resolvers?: ResolverRegistry;
3512
- router?: ExecutionRouter;
3513
- toolSelector?: ToolSelector;
3514
- formatSelector?: FormatSelector;
3515
- schemaRegistry?: SchemaRegistry;
3516
- constraintBackends?: ConstraintBackend[];
3517
- compressor?: Compressor;
3705
+ defaultMaxOutputTokens?: number;
3518
3706
  /**
3519
- * The numbers behind "compress only when it pays for itself" — in money, not
3520
- * in tokens.
3521
- *
3522
- * All optional, and the default never invents a saving: the layer compresses
3523
- * when the net token saving justifies it, and holds back when compressing
3524
- * would push the prompt below the provider's cacheable minimum — where fewer
3525
- * tokens can mean a larger bill. Declare {@link CompressionEconomics.prefix}
3526
- * with your own measurements to unlock the full decision in both directions.
3707
+ * When true (recommended for measurement), a response whose `usage` block is
3708
+ * missing its counters is treated as a HARD ERROR instead of being coerced to
3709
+ * 0 tokens. An absent usage payload otherwise becomes `actual cost = 0` → a
3710
+ * fabricated 100% saving.
3527
3711
  */
3528
- compressionEconomics?: CompressionEconomics;
3529
- retriever?: Retriever;
3530
- reranker?: Reranker;
3712
+ requireUsage?: boolean;
3531
3713
  /**
3532
- * Tuning for the model cascade (`savings.modelCascade`). All optional; the
3533
- * defaults measure and never spend.
3714
+ * How many `cache_control` breakpoints the adapter may spend.
3715
+ *
3716
+ * The API caps them at 4 per request. The adapter never needs more than 2
3717
+ * (end of system, end of the stable message prefix) and the cap is exposed so
3718
+ * a gateway with a stricter limit can lower it. Raising it above 4 is
3719
+ * pointless: the extra breakpoints are rejected by the API, not ignored.
3534
3720
  */
3535
- modelCascade?: ModelCascadeSettings;
3721
+ maxCacheBreakpoints?: number;
3536
3722
  /**
3537
- * Signed BiVelio license token (from your paid subscription). This is the
3538
- * primary way to license the SDK: `createSavingsLayer` verifies it offline
3539
- * against the embedded public key and builds a {@link licenseGate} from it.
3540
- * Without a valid, active license (and without {@link devUnlicensed}), the
3541
- * pipeline refuses to run — get a key at https://savings.bivelio.com.
3723
+ * When true, requests that the pipeline decided to mark also carry the
3724
+ * request-level `cache_control` (Anthropic's automatic caching): the provider
3725
+ * places one extra breakpoint on the last cacheable block and moves it
3726
+ * forward as the conversation grows, so the history beyond the declared
3727
+ * stable prefix is cached incrementally too. It spends one of the four
3728
+ * breakpoint slots and never fires when the economic gate declined to mark.
3542
3729
  *
3543
- * Surrounding whitespace (the trailing newline of a file or mounted secret),
3544
- * wrapping quotes and a `Bearer ` prefix are stripped before verification, so
3545
- * a key read with `readFileSync` works as-is. Anything INSIDE the key is not
3546
- * touched: a key with a space in the middle is a broken key.
3730
+ * Off by default, and deliberately so: the moving breakpoint writes the
3731
+ * volatile tail of EVERY marked request at the 1.25x write premium. That
3732
+ * pays for itself on conversational or agentic traffic — the next turn reads
3733
+ * the whole history at 0.1x — and loses money on one-shot traffic where
3734
+ * nothing ever reads it back. It is also rejected by legacy Bedrock
3735
+ * deployments. Turn it on when your traffic is multi-turn.
3547
3736
  */
3548
- licenseKey?: string;
3737
+ automaticCache?: boolean;
3549
3738
  /**
3550
- * Override the public key used to verify {@link licenseKey}. An SPKI PEM string,
3551
- * or a kid-addressed {@link LicenseKeyset} (`{ keys: {kid: pem}, defaultKid }`)
3552
- * to support signing-key rotation — legacy no-kid tokens use `defaultKid`, and
3553
- * an unknown kid fails closed. For self-hosting/self-testing; defaults to the
3554
- * build-embedded BiVelio key.
3739
+ * Whether a request carrying BOTH the caller's tools and an output contract
3740
+ * may send `tool_choice: {type: "any"}`, which obliges the model to answer
3741
+ * through SOME tool — its pick — instead of writing prose.
3742
+ *
3743
+ * Defaults to `true`, and the default is the point: without it this exact
3744
+ * combination measured 62% MORE expensive than sending no layer at all, every
3745
+ * single time, because prose forces a second full call to repair. Turning it
3746
+ * off restores the old behaviour for a caller who genuinely needs a plain-text
3747
+ * turn to be reachable while a contract is attached.
3555
3748
  */
3556
- licensePublicKey?: string | LicenseKeyset;
3749
+ forceAnswerThroughTool?: boolean;
3750
+ }
3751
+ declare class AnthropicProvider implements Provider {
3752
+ readonly id: string;
3753
+ private readonly opts;
3754
+ private readonly fetchImpl;
3755
+ constructor(opts?: AnthropicOptions);
3756
+ /** The id Anthropic's API sees: bare at `api.anthropic.com`, as written elsewhere. */
3757
+ private modeloEnElCable;
3758
+ generate(req: ProviderRequest, options?: ProviderStreamOptions): Promise<ProviderResponse>;
3557
3759
  /**
3558
- * Pre-built license gate. **Ignored in the published npm package**, where a
3559
- * {@link licenseKey} verified against the embedded BiVelio public key is the
3560
- * only accepted license — passing a gate there logs a warning and changes
3561
- * nothing. It exists for builds from source (this repo's own tests and
3562
- * benchmarks), where it takes precedence over {@link licenseKey}.
3760
+ * Streaming variant of generate() over the Messages API. The final usage is
3761
+ * read from the closing `message_delta` event (the output counter in
3762
+ * `message_start` is provisional); a stream without it reports
3763
+ * `usage.complete: false`, and with `requireUsage` it throws instead. A body
3764
+ * that ends before the provider signalled completion throws. Requests with an
3765
+ * output contract or declared deferrable are served by generate() and yielded
3766
+ * as one text event.
3563
3767
  */
3564
- licenseGate?: LicenseGate;
3768
+ generateStream(req: ProviderRequest, options?: ProviderStreamOptions): AsyncGenerator<ProviderStreamEvent, void, undefined>;
3769
+ private enviar;
3770
+ private construirCuerpo;
3771
+ private marcarCache;
3772
+ private leer;
3773
+ }
3774
+
3775
+ /**
3776
+ * Trace collector: an ordered log of pipeline decisions with elapsed timings.
3777
+ * Every stage records why it did what it did, so a SavingsReport can be audited
3778
+ * and so ablations can attribute savings to a specific module.
3779
+ */
3780
+ declare class TraceCollector {
3781
+ private readonly events;
3782
+ private readonly start;
3783
+ private readonly now;
3784
+ constructor(now?: () => number);
3785
+ record(stage: string, outcome: string, detail?: Record<string, unknown>): void;
3786
+ elapsed(): number;
3787
+ all(): TraceEvent[];
3788
+ }
3789
+
3790
+ /**
3791
+ * Thrown when the pipeline is asked to run without a valid, active BiVelio
3792
+ * license. This is the fail-closed contract: a paid SDK must refuse to operate
3793
+ * unlicensed, rather than silently serving requests for free.
3794
+ */
3795
+ declare class LicenseError extends Error {
3565
3796
  /**
3566
- * OPT-IN online revocation call-home. When set (alongside {@link licenseKey} and
3567
- * with no explicit {@link licenseGate}), the SDK polls this base's
3568
- * `GET /licenses/revocations` for a signed revocation list and enforces it via a
3569
- * LIVE gate — a license cancelled mid-term stops working without a restart.
3797
+ * Resolved license status — the stable handle for branching on the cause,
3798
+ * since the message text is rewritten whenever it can be made clearer:
3570
3799
  *
3571
- * NEVER-BRICK: the customer request path never blocks on the fetch; the warm poll
3572
- * is fired but not awaited, and any failed/unreachable poll is a no-op that keeps
3573
- * the last-known-good snapshot. Leaving this unset keeps the fully-offline,
3574
- * zero-network default path. Pass the base URL of the licensing service, e.g.
3575
- * `https://savings.bivelio.com`.
3800
+ * - `none` — nothing was configured (no `licenseKey`, no gate).
3801
+ * - `invalid` — a key was configured but does not verify.
3802
+ * - `expired` / `grace` — it verifies, but the term is over.
3803
+ * - `revoked` — cancelled by the control plane.
3804
+ * - `stale` — paid and current, but unconfirmed: a connectivity problem.
3805
+ *
3806
+ * The same value is repeated at the end of the message (`license status: X`)
3807
+ * because an uncaught error prints nothing else.
3576
3808
  */
3577
- licenseServerUrl?: string;
3809
+ readonly status?: string;
3810
+ constructor(message: string, status?: string);
3811
+ }
3812
+
3813
+ /** Decoded revocation-list body (mirror of @bivelio/protocol's RevocationListPayload). */
3814
+ interface RevocationListBody {
3815
+ v: number;
3816
+ asOf: number;
3817
+ seq: number;
3818
+ revoked: string[];
3819
+ iss?: string;
3820
+ }
3821
+ interface RevocationListVerification {
3822
+ /** True only if signature + typ + monotonic-seq + skew all pass. */
3823
+ ok: boolean;
3824
+ /** List revision (monotonic). -1 when not ok. */
3825
+ seq: number;
3826
+ /** Unix seconds the list was minted. 0 when not ok. */
3827
+ asOf: number;
3828
+ /** Revoked jti in this list (empty when not ok). */
3829
+ revoked: Set<string>;
3830
+ reason?: string;
3831
+ }
3832
+ /**
3833
+ * Verify a signed revocation-list token offline and enforce the client-side
3834
+ * policy: (1) valid Ed25519 signature, (2) header `typ` is a revocation list
3835
+ * (a license token can't be replayed here), (3) `seq >= minSeq` — reject a
3836
+ * stale/rolled-back list, (4) `asOf <= now + maxSkew` — reject a future-dated list.
3837
+ * ANY failure returns `ok:false` with an empty set; the poller then keeps its
3838
+ * sticky last-known-good (fail-open), never dropping known revocations.
3839
+ */
3840
+ declare function verifyRevocationList(token: string, publicKey: string | LicenseKeyset, opts?: {
3841
+ minSeq?: number;
3842
+ nowSeconds?: number;
3843
+ maxSkewSeconds?: number;
3844
+ }): RevocationListVerification;
3845
+ /**
3846
+ * The SDK's live, sticky, monotonic view of the revocation set. It only ever
3847
+ * GROWS (a jti, once seen revoked, stays revoked for the process lifetime) and
3848
+ * only advances on a higher-or-equal `seq` — so a stale, rolled-back, or empty
3849
+ * list can never un-revoke a license. The live gate reads {@link snapshot} on
3850
+ * every request; the poller feeds it via {@link apply}.
3851
+ */
3852
+ declare class RevocationSnapshot {
3853
+ private _seq;
3854
+ private _asOf;
3855
+ private _lastContactAt;
3856
+ private readonly set;
3857
+ get seq(): number;
3858
+ get asOf(): number;
3859
+ /** Unix seconds of the last CONFIRMED authorized heartbeat (a 304 or a verified
3860
+ * list). Feeds the live gate's online staleness check; 0 until the first one. */
3861
+ get lastContactAt(): number;
3862
+ /** Record a confirmed authorized heartbeat from the control plane (monotonic). */
3863
+ markContact(nowSeconds: number): void;
3864
+ has(jti: string): boolean;
3865
+ /** Live view of the revoked set (the same Set instance — read-only to callers). */
3866
+ snapshot(): ReadonlySet<string>;
3578
3867
  /**
3579
- * Steady-state ceiling between revocation polls, in seconds (default 900 = 15 min).
3580
- * The effective cadence tightens near license expiry and has a hard floor.
3868
+ * Merge an already-verified list. Accepts only `seq >= current seq`; UNIONS the
3869
+ * jti (never removes); advances seq/asOf monotonically. Returns whether the
3870
+ * revoked set grew or the revision advanced.
3581
3871
  */
3582
- licensePollIntervalSeconds?: number;
3583
- /** Injectable `fetch` for the revocation poller (defaults to the global). Testing/proxy. */
3584
- licenseFetch?: typeof fetch;
3585
- /** Advanced tuning of the revocation poller cadence + skew tolerance. */
3586
- licenseRevocation?: {
3587
- /** Hard floor between polls, seconds (default 300). */
3588
- floorSeconds?: number;
3589
- /** Backoff ceiling during a control-plane outage, seconds (default 3600). */
3590
- maxIntervalSeconds?: number;
3591
- /** Jitter fraction ± (default 0.1). */
3592
- jitterRatio?: number;
3593
- /** Reject a signed list whose `asOf` is more than this far in the future, seconds (default 300). */
3594
- maxSkewSeconds?: number;
3595
- /**
3596
- * ONLINE staleness ceiling, seconds (default 72h, `DEFAULT_MAX_STALENESS_SECONDS`). If the SDK goes this long
3597
- * without a CONFIRMED authorized heartbeat (a 304 or a verified list), the LIVE
3598
- * gate stops granting — closing the fail-open window where blocking egress keeps
3599
- * a revoked license alive until natural expiry. Must exceed the poll cadence
3600
- * with margin. Applies ONLY on the online path ({@link licenseServerUrl} set);
3601
- * it is NOT a zero-network opt-out — use the offline path for that.
3602
- */
3603
- maxStalenessSeconds?: number;
3604
- };
3872
+ apply(list: {
3873
+ seq: number;
3874
+ asOf: number;
3875
+ revoked: Iterable<string>;
3876
+ }): boolean;
3877
+ }
3878
+
3879
+ /** A resolved gate that also exposes overall status + a refresh horizon. */
3880
+ interface ResolvedGate extends LicenseGate {
3881
+ isActive(): boolean;
3882
+ status(): LicenseStatus;
3605
3883
  /**
3606
- * Service-account key (`bvk_…`) — your machine credential. It does TWO things,
3607
- * and the second one is why a long-running deployment needs it:
3608
- *
3609
- * 1. **Licence renewal pickup.** Each billing period mints a NEW licence token;
3610
- * the one you deployed expires at the end of the period (+ grace). With this
3611
- * key the SDK fetches the renewed token from the control plane and swaps it
3612
- * in without a restart. WITHOUT it there is no machine pickup, so somebody
3613
- * must paste the new token by hand every cycle or the deployment stops with
3614
- * `LicenseError` once the grace period ends — while the subscription is paid
3615
- * and current. Disable with {@link licenseRefresh}`.enabled = false`.
3616
- * 2. **DETECTIVE usage metering.** COUNTS-ONLY usage after each request —
3617
- * request id, model, cache outcome, and token/cost counts, tagged with an
3618
- * auto-derived machine seat id — NEVER prompts or completions. Disable with
3619
- * {@link metering}`.enabled = false` (which does NOT disable renewal pickup).
3620
- *
3621
- * Both are best-effort and non-blocking: a slow or unreachable control plane
3622
- * never blocks or breaks `generate()`. Get the key from your dashboard.
3884
+ * Why the key does not open, when it does not; `undefined` when it verified.
3885
+ * Under `status() === "invalid"` it tells a key that does NOT EVEN HAVE THE
3886
+ * SHAPE of a token (`"malformed"`: the `bvk_` pasted where the license goes,
3887
+ * arbitrary text) from a well-formed one whose signature fails (the verifier's
3888
+ * reason, e.g. `"bad signature"`). Both are `invalid` — the stable handle for
3889
+ * branching does not change — but the advice given to the customer differs.
3623
3890
  */
3624
- serviceAccountKey?: string;
3891
+ reason(): string | undefined;
3892
+ /** Unix seconds after which the online status should be re-checked (poller anchor). */
3893
+ refreshAfter?(): number | undefined;
3894
+ }
3895
+ /**
3896
+ * Build a LIVE license gate from a signed key. The token is verified + decoded
3897
+ * ONCE (no per-request Ed25519 verify — hot-path safe); each `isActive()` /
3898
+ * `status()` / `isEntitled()` then re-derives the status from the cached payload +
3899
+ * the LIVE clock + the (mutable, sticky) {@link RevocationSnapshot}. This is what
3900
+ * lets a revocation — or an expiry crossed mid-process — take effect without a
3901
+ * restart, while the customer request path never touches the network (it reads an
3902
+ * in-memory Set).
3903
+ *
3904
+ * The offline path passes a fresh, never-updated snapshot (live clock, zero
3905
+ * network); the online path passes a snapshot a {@link RevocationPoller} feeds.
3906
+ */
3907
+ declare function liveGateFromKey(licenseKey: string, publicKey: string | LicenseKeyset, opts: {
3908
+ revocation: RevocationSnapshot;
3909
+ now?: () => number;
3910
+ /** Online staleness ceiling, seconds. If the gate goes this long without a
3911
+ * confirmed authorized heartbeat, it stops granting (closes the block-egress
3912
+ * fail-open). Undefined = check disabled (the offline path never sets it). */
3913
+ maxStalenessSeconds?: number;
3914
+ /** Freshness anchor (unix seconds); defaults to construction time (boot). */
3915
+ startedAt?: number;
3916
+ }): ResolvedGate;
3917
+
3918
+ /**
3919
+ * Build the SDK's structural {@link import("../contracts/types.js").LicenseGate}
3920
+ * from a signed license key — the object `createSavingsLayer({ licenseKey })`
3921
+ * injects on the OFFLINE path (no `licenseServerUrl`).
3922
+ *
3923
+ * It is a LIVE gate over an EMPTY, never-updated revocation snapshot: zero network,
3924
+ * but the clock is LIVE, so a license that crosses its expiry (past grace) stops
3925
+ * working mid-process instead of only after a restart — closing the frozen-clock
3926
+ * hole the old one-shot resolution left open. `isActive()` is true while active or
3927
+ * in grace; `isEntitled` additionally requires the feature in the token's grant.
3928
+ * Online revocation is added by pairing {@link liveGateFromKey} with a
3929
+ * poller-fed snapshot, which `createSavingsLayer` wires up for you.
3930
+ */
3931
+ declare function gateFromKey(licenseKey: string, publicKey: string | LicenseKeyset, now?: () => number): ResolvedGate;
3932
+
3933
+ type RevocationPollEvent = {
3934
+ type: "applied";
3935
+ seq: number;
3936
+ size: number;
3937
+ } | {
3938
+ type: "unchanged";
3939
+ seq: number;
3940
+ } | {
3941
+ type: "rejected";
3942
+ reason: string;
3943
+ } | {
3944
+ type: "error";
3945
+ error: unknown;
3946
+ } | {
3947
+ type: "no-fetch";
3948
+ };
3949
+ interface RevocationPollerOptions {
3950
+ /** Absolute URL of GET /licenses/revocations. */
3951
+ url: string;
3952
+ /** SPKI PEM (or a kid-addressed {@link LicenseKeyset}) used to verify the signed
3953
+ * list — the same key/keyset that verifies license tokens. */
3954
+ publicKeyPem: string | LicenseKeyset;
3955
+ /** The sticky snapshot to feed. */
3956
+ snapshot: RevocationSnapshot;
3957
+ /** Anchor cadence to the license refresh horizon (unix seconds). */
3958
+ getRefreshAfter?: () => number | undefined;
3959
+ /** Injectable clock (unix seconds). */
3960
+ nowSeconds?: () => number;
3961
+ /** Injectable fetch (defaults to the global). */
3962
+ fetchImpl?: typeof fetch;
3963
+ pollIntervalSeconds?: number;
3964
+ floorSeconds?: number;
3965
+ maxIntervalSeconds?: number;
3966
+ jitterRatio?: number;
3967
+ maxSkewSeconds?: number;
3968
+ requestTimeoutMs?: number;
3969
+ /** Injectable RNG in [0,1) for jitter. Default Math.random. */
3970
+ random?: () => number;
3971
+ /** Optional observer (telemetry/tests). */
3972
+ onEvent?: (e: RevocationPollEvent) => void;
3625
3973
  /**
3626
- * Automatic licence-renewal pickup (on by default when {@link serviceAccountKey}
3627
- * and {@link licenseServerUrl} are both set).
3628
- *
3629
- * Turning it OFF means you take responsibility for deploying the renewed token
3630
- * each billing period; if nobody does, the SDK stops at the end of the grace
3631
- * period even though the subscription is paid.
3974
+ * Silencia los avisos por consola. Por defecto están ENCENDIDOS, y ese defecto
3975
+ * es la corrección: el sondeo es fail-open, así que un call-home roto no rompe
3976
+ * nada visible — la lista de revocaciones simplemente se queda vieja, en
3977
+ * silencio, durante días. Quien tenga observabilidad propia puede apagarlos;
3978
+ * quien no la tenga, que es casi todo el mundo, se entera igual.
3632
3979
  */
3633
- licenseRefresh?: {
3634
- enabled?: boolean;
3635
- /** Check cadence in ms (default 3600000 = 1 h). */
3636
- intervalMs?: number;
3637
- /** How long before expiry to start looking, seconds (default 7 days). */
3638
- leadSeconds?: number;
3639
- };
3980
+ silenceWarnings?: boolean;
3640
3981
  /**
3641
- * Usage-ingest URL. Defaults to `${licenseServerUrl}/api/metering/usage`. Set
3642
- * explicitly to point metering at a different host than the revocation poller.
3982
+ * @deprecated Spanish name that leaked into the public options. Use
3983
+ * {@link RevocationPollerOptions.silenceWarnings}. Honoured for one minor
3984
+ * version so nobody gets their warnings back by surprise.
3643
3985
  */
3644
- meteringUrl?: string;
3645
- /** Injectable `fetch` for metering (defaults to {@link licenseFetch}, then global). */
3646
- meteringFetch?: typeof fetch;
3647
- /** DETECTIVE metering tuning (all optional). */
3648
- metering?: {
3649
- /**
3650
- * Off switch for USAGE REPORTING even when {@link serviceAccountKey} is set
3651
- * (default: on). It does NOT disable licence-renewal pickup — that has its
3652
- * own switch, {@link licenseRefresh}`.enabled`, because turning off telemetry
3653
- * must not silently stop your licence from renewing.
3654
- */
3655
- enabled?: boolean;
3656
- /** Flush cadence in ms (default 15000). */
3657
- flushIntervalMs?: number;
3658
- /** Records per POST (default 50). */
3659
- maxBatch?: number;
3660
- /** Hard in-memory cap; oldest records dropped past it (default 1000). */
3661
- maxBuffer?: number;
3662
- /** Override the auto-derived machine seat id. */
3663
- seatId?: string;
3664
- /** Override CI auto-detection (CI/ephemeral seats are excluded from billable seats). */
3665
- ci?: boolean;
3666
- /** Optional org hint; the server derives the authoritative org from the key. */
3667
- orgId?: string;
3668
- };
3669
- /** Opt-in A/B verification on your own traffic. Off by default. See VerificationSettings. */
3670
- verification?: VerificationSettings;
3986
+ silenciarAvisos?: boolean;
3987
+ }
3988
+ /**
3989
+ * PURE, unit-testable next-delay policy (seconds). Steady state anchors to the
3990
+ * license refresh horizon, clamped to [floor, ceiling] and jittered. After a
3991
+ * failure it backs off exponentially (a revocation can't land during an outage
3992
+ * anyway), capped, still jittered, never below the floor.
3993
+ */
3994
+ declare function computeNextDelay(opts: {
3995
+ refreshAfter?: number;
3996
+ nowSeconds: number;
3997
+ consecutiveFailures: number;
3998
+ floorSeconds: number;
3999
+ pollIntervalSeconds: number;
4000
+ maxIntervalSeconds: number;
4001
+ jitterRatio: number;
4002
+ random?: () => number;
4003
+ }): number;
4004
+ declare class RevocationPoller {
4005
+ private readonly opts;
4006
+ private timer;
4007
+ private controller;
4008
+ private consecutiveFailures;
4009
+ private stopped;
4010
+ private started;
4011
+ private warnedNoFetch;
4012
+ /** ETag of the last 200; sent as If-None-Match so an unchanged list 304s (cheap no-op). */
4013
+ private lastEtag;
4014
+ /** 304 seguidos; al llegar al tope se fuerza una descarga firmada. */
4015
+ private consecutive304;
4016
+ constructor(opts: RevocationPollerOptions);
4017
+ private now;
4018
+ private resolveFetch;
3671
4019
  /**
3672
- * Development-only escape hatch to run ungated, honored ONLY when building from
3673
- * source (local dev, CI, benchmarks) — also via the `BVSALA_DEV_OPEN=1`
3674
- * environment variable.
3675
- *
3676
- * IMPORTANT: this flag is **disabled in the published npm package**. The
3677
- * distributed build hard-codes the licensing check on, so setting
3678
- * `devUnlicensed` (or `BVSALA_DEV_OPEN`) there has NO effect — the layer still
3679
- * requires a valid `licenseKey`. It is not a way to run the product for free.
4020
+ * One poll cycle: fetch → verify → apply. NEVER throws (fail-open). A reachable
4021
+ * CP resets the failure backoff even if the returned list is rejected — we keep
4022
+ * last-known-good. Only a transport/abort/parse error counts as a failure.
3680
4023
  */
3681
- devUnlicensed?: boolean;
3682
- /** Per-model cost models; falls back to `defaultCostModel`. */
3683
- costModels?: Record<string, CostModel>;
3684
- defaultCostModel?: CostModel;
3685
- /** TTL for exact-cache writes, in ms. */
3686
- exactCacheTtlMs?: number;
4024
+ pollOnce(): Promise<void>;
4025
+ /** El interruptor, aceptando todavía el nombre viejo. */
4026
+ private callado;
4027
+ /** Un aviso por causa y por proceso, respetando el interruptor. */
4028
+ private avisar;
3687
4029
  /**
3688
- * Refuse the single repair retry when paying for it would push this request's
3689
- * cost above what it would have cost WITHOUT the layer. The invalid answer is
3690
- * then returned as-is with `constraints.structurallyValid: false`, plus a
3691
- * warning naming the three figures.
3692
- *
3693
- * OFF by default, and the default is a deliberate, measured choice rather than
3694
- * caution — read this before turning it on.
3695
- *
3696
- * The bound sounds narrow ("only the worst case") and is not. The baseline of
3697
- * a call that ran is reconstructed EXCLUDING the repair (see `estimateCost`:
3698
- * `actualInput - repairInput`), precisely so a retry cannot pay for itself in
3699
- * the report. That makes the threshold unreachable by construction: the repair
3700
- * only ever moves `actual`, never the baseline, so ANY repair breaches it
3701
- * whenever the layer's other books came out flat. On a request where nothing
3702
- * was compressed, cached or eliminated, `baseline === actual` exactly and the
3703
- * gate suppresses every repair, however cheap.
3704
- *
3705
- * So this is not "cap the tail", it is "stop repairing unless the layer has
3706
- * already banked more than the repair will cost". That is a real quality
3707
- * policy, not a rounding rule, and it is the caller's contract to decide on —
3708
- * hence opt-in.
4030
+ * THE CONTROL PLANE HAS BEEN UNREACHABLE FOR A WHILE.
3709
4031
  *
3710
- * There is no threshold that fires on the pathological case and spares the
3711
- * flat one; they are the same arithmetic at different magnitudes. And there is
3712
- * a genuine argument on the other side: an answer that misses its contract is
3713
- * a MODEL failure, so the no-layer path would have had to pay for its own
3714
- * second call too — charging the whole retry against the layer compares one
3715
- * valid answer to one invalid answer.
4032
+ * Polling is fail-open by design: a network outage must never take down the
4033
+ * customer's production traffic. The trade-off is that an unreachable control
4034
+ * plane has no visible symptom — the license keeps working and requests keep
4035
+ * flowing while the revocation list quietly goes stale.
3716
4036
  *
3717
- * Turn it on when a hard per-request ceiling matters more than contract
3718
- * compliance — a batch backfill, a cost-capped tenant. Leave it off when the
3719
- * caller relies on the shape being right.
4037
+ * So the warning goes to the console BY DEFAULT, not only through the optional
4038
+ * `onEvent` observer. It fires on the third consecutive failure and then every
4039
+ * twelfth: a lost request is normal, a week-long outage must keep reminding.
4040
+ * Silence it with `silenceWarnings` if you have your own observability.
3720
4041
  */
3721
- skipRepairAboveBaseline?: boolean;
4042
+ private avisarFalloDeRed;
4043
+ /** Fire one non-blocking warm poll, then self-schedule. Idempotent. */
4044
+ start(): void;
4045
+ private schedule;
4046
+ /** Stop polling and abort any in-flight request. Idempotent. */
4047
+ stop(): void;
4048
+ }
4049
+
4050
+ interface UsageEmitterOptions {
4051
+ /** Full ingest URL, e.g. `https://savings.bivelio.com/api/metering/usage`. */
4052
+ url: string;
4053
+ /** Service-account key (`bvk_…`) sent as a Bearer token; the server derives the org from it. */
4054
+ serviceAccountKey: string;
4055
+ /** Optional org hint; the server always derives the authoritative org from the key. */
4056
+ orgId?: string;
4057
+ /** Flush cadence in ms (default 15000). */
4058
+ flushIntervalMs?: number;
4059
+ /** Records per POST (default 50). */
4060
+ maxBatch?: number;
4061
+ /** Hard in-memory cap; oldest records are dropped past it (default 1000). */
4062
+ maxBuffer?: number;
4063
+ /** Injectable fetch (defaults to the global). */
4064
+ fetchImpl?: typeof fetch;
4065
+ /** Injectable clock. */
4066
+ now?: () => number;
4067
+ /** Whether this process is a CI/ephemeral runner (excluded from billable seats). */
4068
+ ci?: boolean;
3722
4069
  /**
3723
- * Reliability contract. When true (the default), ANY internal optimizer error
3724
- * (a resolver, cache, format selector, compressor, or accounting bug) is
3725
- * caught and the request falls open to a plain provider call — BV-SALA must
3726
- * never add a crash point to the customer's request path. Set to `false` only
3727
- * in tests/benchmarks that want internal errors to surface.
4070
+ * Which surface owns this process: the local gateway (`bvsala claude` and
4071
+ * friends) or the in-process SDK. Sent with the version in the batch's
4072
+ * `x-bvsala-version` header (`gateway/0.4.16`, `sdk/0.4.16`) so the control
4073
+ * plane can tell a gateway that lacks a lever from an SDK that never had it.
4074
+ * Defaults to `"sdk"`; the gateway sets `"gateway"`.
3728
4075
  */
3729
- failOpen?: boolean;
4076
+ surface?: "gateway" | "sdk";
3730
4077
  /**
3731
- * Silence BV-SALA's console warnings. They are ON by default, and they only
3732
- * ever report a condition whose symptom is otherwise invisible: a saving knob
3733
- * asked for but not wired up, usage that stops reaching the control plane, a
3734
- * revocation list going stale. Nothing here throws and nothing here changes
3735
- * what the layer does — turning it on only removes the console noise, so set
3736
- * it when you have your own observability. Applies to the pipeline, the usage
3737
- * emitter and the revocation poller alike.
4078
+ * Se llama cuando el ingest rechaza la credencial (401/403). Existe para que un
4079
+ * despliegue con observabilidad pueda ENTERARSE: el aviso por consola sirve en
4080
+ * un portátil, pero en producción los `console.warn` no despiertan a nadie, y
4081
+ * este fallo concreto es invisible por definición —deja de llegar uso, que es
4082
+ * exactamente lo mismo que se ve cuando no hay tráfico—.
3738
4083
  */
3739
- silenceWarnings?: boolean;
4084
+ onAuthError?: (status: number) => void;
3740
4085
  /**
3741
- * Load the exact OpenAI tokenizer (`gpt-tokenizer`, optional peer) into the
3742
- * default registry when it is installed. ON by default: without it, every
3743
- * saving where the layer removed something is reported as `estimated` and
3744
- * never billed, and the SDK warns once. Set to `false` to keep the built-in
3745
- * heuristic even when the package is present (tests calibrated on the
3746
- * heuristic, or a process that must not spend the encoding's memory). Ignored
3747
- * when you pass your own `tokenizer` registry: you decide what it contains.
4086
+ * Silence the console warnings. They are ON by default, exactly as in the
4087
+ * revocation poller: metering is best-effort, so a broken ingest breaks
4088
+ * nothing visible — usage simply stops arriving, silently, and the customer's
4089
+ * dashboard stays empty forever.
3748
4090
  */
3749
- exactTokenizer?: boolean;
3750
- /** Injectable clock for deterministic tests. */
3751
- now?: () => number;
3752
- /** Injectable id generator for deterministic tests. */
3753
- generateId?: () => string;
4091
+ silenceWarnings?: boolean;
3754
4092
  }
3755
4093
  /**
3756
- * Tuning for the model cascade. Everything here is optional, and the defaults
3757
- * cost nothing: with no settings at all, `modelCascade: "shadow"` measures what
3758
- * it can measure for free and makes no extra provider call.
4094
+ * Buffers usage records (counts, cost figures and opaque identifiers, never
4095
+ * content) and flushes them to the control plane. DETECTIVE
4096
+ * metering: strictly best-effort and NON-BLOCKING. `record()` is O(1) and
4097
+ * `flush()` never throws, so a slow, unreachable, or erroring control plane can
4098
+ * neither block nor crash the customer's request path. The buffer is hard-capped
4099
+ * so a sustained outage can never grow memory without bound.
3759
4100
  */
3760
- interface ModelCascadeSettings {
4101
+ declare class UsageEmitter {
4102
+ private readonly options;
4103
+ private readonly now;
4104
+ private readonly flushIntervalMs;
4105
+ private readonly maxBatch;
4106
+ private readonly maxBuffer;
4107
+ private buffer;
4108
+ private seatId;
4109
+ private ci;
4110
+ private timer;
4111
+ private inflight;
3761
4112
  /**
3762
- * Your own strong → cheap pairs, replacing the built-in table.
3763
- *
3764
- * Keys and values are model ids WITHOUT a vendor prefix (`"gpt-4o"`, not
3765
- * `"openai/gpt-4o"`); the caller's prefix is carried over automatically, and a
3766
- * dated snapshot inherits from its base id. A pair whose economics do not work
3767
- * out at your prices is refused with a reason in the report — declaring it here
3768
- * does not override the arithmetic.
4113
+ * CONSECUTIVE failed flushes. Warn on the third and then every twelfth: one
4114
+ * lost batch is normal, a whole hour is not. Same cadence as the revocation
4115
+ * poller on purpose — two different rhythms for the same class of failure
4116
+ * only confuse whoever reads the logs.
3769
4117
  */
3770
- pairs?: Record<string, string>;
4118
+ private fallosSeguidos;
4119
+ constructor(options: UsageEmitterOptions);
4120
+ /** Set the resolved seat identity (derivation is async; records emitted before
4121
+ * it resolves are stamped at flush time, so none are lost). */
4122
+ setSeat(seatId: string | undefined, ci: boolean): void;
4123
+ /** Record one request's usage. Never blocks; O(1). */
4124
+ record(report: SavingsReport): void;
4125
+ start(): void;
4126
+ /** Stop the timer and attempt a final flush. Safe to call more than once.
4127
+ * Fire-and-forget: for a CLEAN shutdown that must not lose the last batch,
4128
+ * use `stopAndDrain` and await it. */
4129
+ stop(): void;
4130
+ /** Clean shutdown: stop the timer and DRAIN the buffer, awaiting each POST.
4131
+ * Unlike `stop()` (fire-and-forget), this resolves only once the final batch
4132
+ * has actually been sent, so a graceful exit doesn't lose the last session's
4133
+ * usage. Bounded, so an unreachable control plane can't block shutdown. */
4134
+ stopAndDrain(maxIntentos?: number): Promise<void>;
3771
4135
  /**
3772
- * How many requests of a workload are routed before the automatic shut-off
3773
- * judges the measured escalation rate (default 20).
3774
- *
3775
- * The rate is only observable on requests that were actually routed, so there
3776
- * is no way to know without spending a bounded amount to find out. Worst case
3777
- * (every one of them escalates) that exploration costs `n x r` requests'
3778
- * worth of extra spend, once per workload per process — with a ratio of 0.06
3779
- * and the default, about 1.2 requests.
4136
+ * Flush one batch. Never throws. A transient server error (5xx) or network
4137
+ * failure re-queues the batch (capped); a 4xx (bad request / bad key) drops it,
4138
+ * since retrying a rejected payload would loop forever.
3780
4139
  */
3781
- exploration?: number;
4140
+ flush(): Promise<void>;
4141
+ private hacerFlush;
3782
4142
  /**
3783
- * PRICED OPT-IN, default 0 (off). Fraction of `"shadow"` requests on which the
3784
- * layer also calls the cheap model, purely to measure.
3785
- *
3786
- * Shadow mode is free precisely because it does not run the cheap model: it
3787
- * reports eligibility, the arithmetic, and what the swap would have been worth.
3788
- * What it cannot know for free is the one number that decides everything — how
3789
- * often the deterministic gate would have REJECTED the cheap answer. That
3790
- * answer does not exist until the cheap model writes one.
3791
- *
3792
- * So this buys it, and the price is exact: each sampled request costs one extra
3793
- * cheap call, i.e. `r` of what the request already cost — about 6% for
3794
- * gpt-4o → gpt-4o-mini. At `0.05` that is roughly 0.3% on top of your bill, and
3795
- * every sampled request reports its own `modelCascade.sampleCost`. It also
3796
- * DOUBLES that request's provider latency, because the extra call is sequential.
4143
+ * THE INGEST HAS BEEN FAILING FOR A WHILE AND NOBODY NOTICED.
3797
4144
  *
3798
- * The sampled request's tokens count as spend and are held out of the baseline,
3799
- * exactly like the repair retry: measurement must never pay for itself.
4145
+ * Metering is best-effort by design — it must never take down the customer's
4146
+ * request — and that turns every failure into SILENT data loss. Without this,
4147
+ * the only symptom is an empty dashboard, which is indistinguishable from
4148
+ * "you haven't sent anything yet": the symptom points the opposite way from
4149
+ * the cause.
3800
4150
  *
3801
- * Which requests get sampled is derived from the request's own cache key, so it
3802
- * is deterministic and stable — the same request is always sampled or always
3803
- * not, and no `Math.random` decides how your money is spent.
4151
+ * Warns on the third consecutive failure and then every twelfth. Not the
4152
+ * first — a lost request is normal — and not every one, which becomes noise.
3804
4153
  */
3805
- shadowSampleRate?: number;
4154
+ private avisarDeFallo;
4155
+ private requeue;
4156
+ private toWire;
3806
4157
  }
3807
- declare const DEFAULT_COST_MODEL: CostModel;
4158
+
4159
+ /** True when running in a CI / ephemeral environment. */
4160
+ declare function detectCi(): boolean;
4161
+ /**
4162
+ * Derive the machine seat id for a service-account key. Resolves to `undefined`
4163
+ * when no machine fingerprint is available (e.g. a browser) — the caller then
4164
+ * reports no seatId and the server counts the request under an "unknown" seat.
4165
+ * The id (`m-<32 hex>`) matches the control plane's identity charset.
4166
+ */
4167
+ declare function deriveSeatId(serviceAccountKey: string): Promise<string | undefined>;
3808
4168
 
3809
4169
  /** Fully-resolved, non-optional dependency set the pipeline runs against. */
3810
4170
  interface ResolvedDeps {
@@ -3894,4 +4254,373 @@ declare class StreamAbortedError extends Error {
3894
4254
  constructor(requestSent: boolean);
3895
4255
  }
3896
4256
 
3897
- 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, DEFAULT_MAX_STALENESS_SECONDS, 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, InMemoryToolSetFreezer, type JsonSchema, LicenseError, type LicenseGate, type LicenseKeyShape, 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 ProviderStreamEvent, type ProviderStreamOptions, type ProviderUsage, QUALITY_STATUSES, type QualityStatus, REFERENCE_MAX_AGE_MS, REFERENCE_MESSAGE_ALLOWANCE, REFERENCE_OUTPUT_SHARE, type ReferenceCredit, type ReferenceRejection, 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 SavingsStream, type SavingsStreamEvent, type SavingsStreamOptions, type SavingsToggles, SchemaRegistry, type SemanticCacheOptions, type SemanticHit, type SemanticRecord, type SemanticVerifier, type SerializationFormat, type Serializer, Singleflight, StreamAbortedError, type StreamingReport, type TokenEstimator, TokenizerRegistry, type ToolCallRequest, type ToolDescriptor, type ToolRisk, type ToolSelection, ToolSelector, type ToolSelectorOptions, type ToolSetFreezer, TraceCollector, type TraceEvent, UMBRAL_LOCAL, UsageEmitter, type UsageEmitterOptions, type ValidationError, type ValidationResult, type VerificationCheck, type VerificationExclusion, type VerificationOutcome, type VerificationReport, type VerificationSettings, type VerificationSide, type VerificationStratum, adaptiveK, arithmeticResolver, buildExactKey, buildInflightKey, canonicalize, compactRowsSerializer, compresionFiel, computeNextDelay, cosine, createCompactRowsSerializer, createDefaultResolvers, createHandoff, createMockProvider, createSavingsLayer, csvSerializer, decodeCompactRows, dedupe, defaultTokenizerRegistry, deriveSeatId, deriveStatus, detectCi, embebedorLocal, emptyState, encodeCompactRows, evalArithmetic, evaluatePolicy, executeRetrievalPlan, gateFromKey, heuristicEstimator, isKeyset, isStrictCompatible, jsonCompactSerializer, keyTokensMatch, licenseKeyShape, liveGateFromKey, mergeDelta, modelFamilyOf, multisetEqual, negationCount, noopCompressor, normalizeLicenseKey, normalizeText, numericTokens, precioDeLista, projectContext, providerNativeBackend, quitarPrefijoDeProveedor, randomId, registerExactTokenizer, registrarTokenizadorExacto, renderState, resolveConstraintBackend, resolveDependencies, resolveLicense, sha256, shouldCompress, stableStringify, uniformColumns, validateAgainstSchema, vectorLocal, verifyCompactSignature, verifyLicenseToken, verifyRevocationList, withProtectedSpans, words };
4257
+ /**
4258
+ * The parts of a SavingsReport the exporters read. A full SavingsReport
4259
+ * satisfies it, and so does a stored copy without `traces`: the exporters never
4260
+ * read `traces`, messages, answers or tool arguments.
4261
+ */
4262
+ type SavingsReportForExport = Pick<SavingsReport, "requestId" | "model" | "calls" | "tokens" | "cache" | "cost"> & Partial<Pick<SavingsReport, "qualityStatus" | "modelCascade" | "latencyMs" | "streaming">>;
4263
+ /**
4264
+ * One report and when its request was made: an ISO 8601 string, epoch
4265
+ * milliseconds or a Date. A SavingsReport carries no timestamp of its own, so
4266
+ * record `at` when the report comes back (`Date.now()` is fine).
4267
+ */
4268
+ interface SavingsRecord {
4269
+ at: string | number | Date;
4270
+ report: SavingsReportForExport;
4271
+ }
4272
+ /** An attribute value these exporters write: never null, never an object. */
4273
+ type SpanAttributeValue = string | number | boolean | string[];
4274
+ /** The attributes these exporters write on a span. */
4275
+ type SavingsSpanAttributes = Record<string, SpanAttributeValue>;
4276
+ /**
4277
+ * The part of an OpenTelemetry `Span` these exporters use. A `Span` from
4278
+ * `@opentelemetry/api` satisfies it.
4279
+ */
4280
+ interface SpanLike {
4281
+ setAttributes(attributes: SavingsSpanAttributes): unknown;
4282
+ setStatus?(status: {
4283
+ code: number;
4284
+ message?: string;
4285
+ }): unknown;
4286
+ end(): void;
4287
+ isRecording?(): boolean;
4288
+ }
4289
+ /** The part of OpenTelemetry's `SpanOptions` these exporters pass. */
4290
+ interface SpanStartOptionsLike {
4291
+ kind?: number;
4292
+ attributes?: SavingsSpanAttributes;
4293
+ }
4294
+ /** The part of an OpenTelemetry `Tracer` these exporters use. */
4295
+ interface TracerLike {
4296
+ startActiveSpan<T>(name: string, options: SpanStartOptionsLike, fn: (span: SpanLike) => T): T;
4297
+ }
4298
+ /** `trace` from @opentelemetry/api, or any TracerProvider. */
4299
+ interface TracerProviderLike {
4300
+ getTracer(name: string, version?: string): TracerLike;
4301
+ }
4302
+ /** Options shared by every function that writes span attributes. */
4303
+ interface SavingsAttributeOptions {
4304
+ /**
4305
+ * ISO 4217 code of the report's money. Default "USD", the unit of the
4306
+ * built-in price catalog. If you configured `costModels` in another unit, say
4307
+ * so here: the report itself does not carry a currency.
4308
+ */
4309
+ currency?: string;
4310
+ /**
4311
+ * Mirror the figures as filterable Langfuse metadata
4312
+ * (`langfuse.observation.metadata.bivelio_*`, string values). `cost` also sets
4313
+ * `langfuse.observation.cost_details` to the report's actual cost, which
4314
+ * Langfuse then prefers over its own estimate; Langfuse reads that figure as
4315
+ * USD, so it is only written when `currency` is "USD". Both off by default.
4316
+ */
4317
+ langfuse?: {
4318
+ metadata?: boolean;
4319
+ cost?: boolean;
4320
+ };
4321
+ /**
4322
+ * Mirror the figures in `_dd.ml_obs.metadata` for Datadog LLM Observability,
4323
+ * as a JSON object under the key `bivelio`. Off by default: without it the
4324
+ * `com.bivelio.*` attributes still arrive in Datadog as tags.
4325
+ */
4326
+ datadog?: {
4327
+ metadata?: boolean;
4328
+ };
4329
+ }
4330
+ /** Options for {@link recordSavingsOnSpan}. */
4331
+ interface SavingsSpanOptions extends SavingsAttributeOptions {
4332
+ /**
4333
+ * Also write the GenAI usage attributes and `gen_ai.response.model`.
4334
+ * Default false: the span you pass may be an HTTP or job span, and marking it
4335
+ * as an LLM call would misclassify it in tools that key on `gen_ai.*`.
4336
+ */
4337
+ genAi?: boolean;
4338
+ }
4339
+ /** Options for {@link instrumentSavingsLayer}. */
4340
+ interface SavingsTracingOptions extends SavingsAttributeOptions {
4341
+ /** `trace` from @opentelemetry/api, a TracerProvider, or a Tracer. */
4342
+ tracer: TracerLike | TracerProviderLike;
4343
+ /**
4344
+ * The `gen_ai.provider.name` value. Default: derived from the model id prefix
4345
+ * (`openai/` gives "openai"); omitted when the model id has no prefix.
4346
+ */
4347
+ providerName?: string;
4348
+ }
4349
+ /**
4350
+ * What to pass to a Langfuse generation's `update()`. `usageDetails` uses
4351
+ * Langfuse's exclusive buckets: `input` does not include
4352
+ * `cache_read_input_tokens`.
4353
+ */
4354
+ interface LangfuseGenerationUpdate {
4355
+ model: string;
4356
+ usageDetails?: Record<string, number>;
4357
+ costDetails?: Record<string, number>;
4358
+ metadata: Record<string, string | number>;
4359
+ }
4360
+ /** Options for {@link toSavingsSeries}. */
4361
+ interface SavingsSeriesOptions {
4362
+ /** Length of each period, in UTC. Default "day". */
4363
+ granularity?: "hour" | "day";
4364
+ /** ISO 4217 code of the reports' money. Default "USD". */
4365
+ currency?: string;
4366
+ /**
4367
+ * Extra grouping labels for each record (team, feature, environment). Keys
4368
+ * must not repeat a column of {@link SavingsSeriesRow}.
4369
+ */
4370
+ dimensions?: (record: SavingsRecord) => Record<string, string> | undefined;
4371
+ }
4372
+ /**
4373
+ * One period, model and basis of the savings series. Money is in `currency`;
4374
+ * each saving column holds money of its own basis only, so summing one column
4375
+ * never mixes measured and estimated figures.
4376
+ */
4377
+ type SavingsSeriesRow = {
4378
+ /** Start of the period, inclusive (`YYYY-MM-DDTHH:mm:ssZ`, UTC). */
4379
+ periodStart: string;
4380
+ /** End of the period, exclusive (`YYYY-MM-DDTHH:mm:ssZ`, UTC). */
4381
+ periodEnd: string;
4382
+ /** The UTC date of `periodStart` (`YYYY-MM-DD`). */
4383
+ date: string;
4384
+ /** The model id as the report names it (the model that served the answer). */
4385
+ model: string;
4386
+ /** Where the money in this row comes from. `shadow` reports are never exported. */
4387
+ basis: Exclude<CostBasis, "shadow">;
4388
+ currency: string;
4389
+ /** Reports in this row. */
4390
+ requests: number;
4391
+ /** LLM calls that reached the provider. */
4392
+ providerCalls: number;
4393
+ /** LLM calls that never executed. */
4394
+ avoidedCalls: number;
4395
+ /** Sum of `cost.baselineEstimate`; null when basis is "unmeasured". */
4396
+ baselineCost: number | null;
4397
+ /** Sum of `cost.actual`; null when basis is "unmeasured". */
4398
+ actualCost: number | null;
4399
+ /** Sum of `baselineEstimate - actual` on "measured" rows; null on every other row. */
4400
+ measuredSavings: number | null;
4401
+ /** Sum of `baselineEstimate - actual` on "reference" rows; null on every other row. */
4402
+ referenceSavings: number | null;
4403
+ /** Sum of `baselineEstimate - actual` on "estimated" rows; null on every other row. */
4404
+ estimatedSavings: number | null;
4405
+ /** Sum of `cost.eliminatedInputFloorCost`, a separate lane; null when no report carried it or basis is "unmeasured". */
4406
+ eliminatedInputFloorCost: number | null;
4407
+ baselineInputTokens: number;
4408
+ actualInputTokens: number;
4409
+ eliminatedInputTokens: number;
4410
+ compressedInputTokens: number;
4411
+ /** Sum of `tokens.providerCachedInput`; null when no report in the row carried it. */
4412
+ providerCachedInputTokens: number | null;
4413
+ baselineOutputTokens: number;
4414
+ actualOutputTokens: number;
4415
+ eliminatedOutputTokens: number;
4416
+ /** The labels returned by `options.dimensions`, empty without it. */
4417
+ dimensions: Readonly<Record<string, string>>;
4418
+ };
4419
+ /** Options for {@link toFocusRows}. */
4420
+ interface FocusExportOptions {
4421
+ /**
4422
+ * Your billing account id with the LLM provider (for example your OpenAI
4423
+ * organization id), so these rows join the provider's own cost rows.
4424
+ * Required: the SDK does not know it.
4425
+ */
4426
+ billingAccountId: string | ((record: SavingsRecord) => string);
4427
+ /** Display name of that billing account, when the provider has one. */
4428
+ billingAccountName?: string | ((record: SavingsRecord) => string | undefined);
4429
+ /** The provider sub account (project, workspace) a record belongs to. */
4430
+ subAccount?: (record: SavingsRecord) => {
4431
+ id: string;
4432
+ name: string;
4433
+ } | undefined;
4434
+ /**
4435
+ * FOCUS `ServiceProviderName`. Default: derived from the model id prefix
4436
+ * (`openai/` gives "OpenAI"). A model id whose provider cannot be derived
4437
+ * throws, because FOCUS does not allow a placeholder here.
4438
+ */
4439
+ serviceProviderName?: string | ((model: string) => string);
4440
+ /** FOCUS `InvoiceIssuerName` (a cloud marketplace or gateway that bills you). Default: the service provider. */
4441
+ invoiceIssuerName?: string | ((model: string) => string);
4442
+ /** FOCUS `HostProviderName` (the cloud hosting the model). Default: the service provider. */
4443
+ hostProviderName?: string | ((model: string) => string);
4444
+ /** FOCUS `ServiceName`. Default "BV-SALA savings ledger". */
4445
+ serviceName?: string;
4446
+ /**
4447
+ * The billing period a charge period belongs to. Default: the calendar month
4448
+ * in UTC. Set it if your provider invoices on another cycle.
4449
+ */
4450
+ billingPeriod?: (chargePeriodStart: Date) => {
4451
+ start: Date;
4452
+ end: Date;
4453
+ };
4454
+ /** Your own tags for a record, added to `Tags`. Keys starting with "bivelio/" are reserved. */
4455
+ tags?: (record: SavingsRecord) => Record<string, string> | undefined;
4456
+ /** Length of each charge period, in UTC. Default "day". */
4457
+ granularity?: "hour" | "day";
4458
+ /** ISO 4217 code of the reports' money. Default "USD". */
4459
+ currency?: string;
4460
+ }
4461
+ /**
4462
+ * A FOCUS 1.4 Cost and Usage row. Never a charge: every cost column is 0, so
4463
+ * joining these rows with your provider's bill adds savings columns and never
4464
+ * adds spend. The savings live in the `x_BiVelio*` columns, one per basis.
4465
+ */
4466
+ type FocusRow = {
4467
+ BilledCost: 0;
4468
+ EffectiveCost: 0;
4469
+ ListCost: 0;
4470
+ ContractedCost: 0;
4471
+ BillingCurrency: string;
4472
+ BillingAccountId: string;
4473
+ BillingAccountName: string | null;
4474
+ BillingPeriodStart: string;
4475
+ BillingPeriodEnd: string;
4476
+ ChargePeriodStart: string;
4477
+ ChargePeriodEnd: string;
4478
+ ChargeCategory: "Adjustment";
4479
+ ChargeClass: null;
4480
+ ChargeDescription: string;
4481
+ ChargeFrequency: "Usage-Based";
4482
+ InvoiceIssuerName: string;
4483
+ ServiceProviderName: string;
4484
+ HostProviderName: string;
4485
+ ServiceName: string;
4486
+ ServiceCategory: "AI and Machine Learning";
4487
+ ServiceSubcategory: "Generative AI";
4488
+ PricingQuantity: null;
4489
+ PricingUnit: null;
4490
+ SubAccountId: string | null;
4491
+ SubAccountName: string | null;
4492
+ /** JSON object: `bivelio/basis`, `bivelio/model` and your own tags. */
4493
+ Tags: string;
4494
+ x_BiVelioBasis: Exclude<CostBasis, "shadow">;
4495
+ x_BiVelioModelId: string;
4496
+ x_BiVelioRequestCount: number;
4497
+ x_BiVelioProviderCallCount: number;
4498
+ x_BiVelioAvoidedCallCount: number;
4499
+ x_BiVelioBaselineCost: number | null;
4500
+ x_BiVelioActualCost: number | null;
4501
+ x_BiVelioMeasuredSavings: number | null;
4502
+ x_BiVelioReferenceSavings: number | null;
4503
+ x_BiVelioEstimatedSavings: number | null;
4504
+ x_BiVelioEliminatedInputFloorCost: number | null;
4505
+ x_BiVelioBaselineInputTokens: number;
4506
+ x_BiVelioActualInputTokens: number;
4507
+ x_BiVelioEliminatedInputTokens: number;
4508
+ x_BiVelioCompressedInputTokens: number;
4509
+ x_BiVelioProviderCachedInputTokens: number | null;
4510
+ x_BiVelioBaselineOutputTokens: number;
4511
+ x_BiVelioActualOutputTokens: number;
4512
+ x_BiVelioEliminatedOutputTokens: number;
4513
+ };
4514
+ /** A value {@link toCsv} can write. null and undefined become an empty field. */
4515
+ type CsvValue = string | number | boolean | null | undefined;
4516
+
4517
+ /**
4518
+ * The `com.bivelio.*` attributes for one report: the four ledgers, the basis,
4519
+ * the cost and the ids. Money is in `options.currency` (default "USD"), and the
4520
+ * money saved goes in one key per basis (`com.bivelio.cost.saved.measured`,
4521
+ * `.reference`, `.estimated`), so a sum over one key never mixes measured and
4522
+ * estimated money. `unmeasured` and `shadow` reports carry their counters and
4523
+ * no money. Nothing here is content: only ids, counts, money and enums.
4524
+ */
4525
+ declare function savingsSpanAttributes(report: SavingsReportForExport, options?: SavingsAttributeOptions): SavingsSpanAttributes;
4526
+
4527
+ /**
4528
+ * Write the savings figures of one report on a span you already have (for
4529
+ * example `trace.getActiveSpan()`), as `com.bivelio.*` attributes. It does not
4530
+ * end the span, never throws and does nothing on a span that is not recording.
4531
+ *
4532
+ * It writes no `gen_ai.*` attribute unless `genAi: true`, and then only the
4533
+ * usage and `gen_ai.response.model`. Tools that key on `gen_ai.*` (Datadog LLM
4534
+ * Observability drops spans without one) need the span from
4535
+ * {@link instrumentSavingsLayer} instead.
4536
+ */
4537
+ declare function recordSavingsOnSpan(span: SpanLike, report: SavingsReportForExport, options?: SavingsSpanOptions): void;
4538
+ /**
4539
+ * Returns a SavingsLayer that opens one OpenTelemetry span per request, with
4540
+ * the same results and the same errors: `generate()` resolves to the very
4541
+ * result object, `generateStream()` returns the very stream.
4542
+ *
4543
+ * The span is named `chat {model}`, has kind CLIENT and is active while the
4544
+ * request runs, so HTTP spans of your provider call nest under it. It carries
4545
+ * the GenAI semantic conventions (as published in OpenTelemetry semantic
4546
+ * conventions v1.41.1: operation, provider, request model and parameters,
4547
+ * response model, usage) and the `com.bivelio.*` savings attributes of
4548
+ * {@link savingsSpanAttributes}. An avoided call is still a span, with zero
4549
+ * usage: no token was billed.
4550
+ *
4551
+ * Nothing is sent anywhere by this function: spans go to the exporters you
4552
+ * configured in your OpenTelemetry SDK. No prompt, answer, tool argument or
4553
+ * error message is written; a failed request records only `error.type`.
4554
+ *
4555
+ * Telemetry never changes a request: if the tracer fails, the request runs
4556
+ * without a span. An invalid `tracer` throws a TypeError here, at startup.
4557
+ */
4558
+ declare function instrumentSavingsLayer(layer: SavingsLayer, options: SavingsTracingOptions): SavingsLayer;
4559
+
4560
+ /**
4561
+ * The fields to pass to a Langfuse generation's `update()` when you trace with
4562
+ * `@langfuse/tracing` by hand: `generation.update(toLangfuseGeneration(report))`.
4563
+ *
4564
+ * - `model` drops a known provider prefix (`openai/gpt-4o-mini` becomes
4565
+ * `gpt-4o-mini`), so Langfuse can match it to its price table.
4566
+ * - `usageDetails` uses exclusive buckets: `input` excludes
4567
+ * `cache_read_input_tokens`. Cache writes are not broken out in the report, so
4568
+ * they stay inside `input`. Left out when the provider reported no usage.
4569
+ * - `costDetails` is `{ total: report.cost.actual }` only with
4570
+ * `includeCost: true`, when `currency` is "USD" (Langfuse reads it as USD) and
4571
+ * the basis has money. It includes the cache-write premium; Langfuse then
4572
+ * uses it instead of its own estimate.
4573
+ * - `metadata` holds the savings figures (`bivelio_*`), one saving key per basis.
4574
+ */
4575
+ declare function toLangfuseGeneration(report: SavingsReportForExport, options?: {
4576
+ currency?: string;
4577
+ includeCost?: boolean;
4578
+ }): LangfuseGenerationUpdate;
4579
+
4580
+ /**
4581
+ * Savings per period (UTC day by default), model, basis and your own
4582
+ * dimensions: the series to load as a business metric or telemetry stream in a
4583
+ * FinOps tool. Same aggregation as {@link toFocusRows}.
4584
+ *
4585
+ * Each saving column holds money of its own basis only, and `shadow` reports
4586
+ * are left out. For a metric that should match your provider invoice, use
4587
+ * `measuredSavings` and `referenceSavings`; `estimatedSavings` has its own
4588
+ * column on purpose. Throws a TypeError on an invalid `at`, currency or
4589
+ * dimension.
4590
+ */
4591
+ declare function toSavingsSeries(records: Iterable<SavingsRecord>, options?: SavingsSeriesOptions): SavingsSeriesRow[];
4592
+
4593
+ /**
4594
+ * Your SavingsReports as FOCUS 1.4 Cost and Usage rows, one per charge period
4595
+ * (UTC day by default), model, basis, billing account, sub account and tag set.
4596
+ *
4597
+ * Every cost column (`BilledCost`, `EffectiveCost`, `ListCost`,
4598
+ * `ContractedCost`) is 0 by design: your provider's bill already carries the
4599
+ * spend, so joining these rows with it adds savings and never adds cost.
4600
+ * `ChargeCategory` is "Adjustment" and nothing here invents a SKU or a price.
4601
+ *
4602
+ * The savings are in custom columns, one per basis: `x_BiVelioMeasuredSavings`,
4603
+ * `x_BiVelioReferenceSavings` and `x_BiVelioEstimatedSavings`; only the column
4604
+ * of the row's basis holds a value. `unmeasured` rows carry counts and null
4605
+ * money, and `shadow` reports are left out. `Tags` carries `bivelio/basis` and
4606
+ * `bivelio/model` plus your own tags.
4607
+ *
4608
+ * Throws a TypeError on an invalid `at`, currency or option, on a user tag
4609
+ * starting with "bivelio/", and when the provider of a model cannot be
4610
+ * derived and `serviceProviderName` is not given.
4611
+ */
4612
+ declare function toFocusRows(records: Iterable<SavingsRecord>, options: FocusExportOptions): FocusRow[];
4613
+
4614
+ /**
4615
+ * Rows to RFC 4180 CSV: CRLF line breaks, quoted fields where needed, null as
4616
+ * an empty field and numbers as plain decimals (no exponent, at most 10
4617
+ * decimal places). The header is `options.columns` or the keys of the rows in
4618
+ * first-seen order. `toCsv([])` is an empty string; with `columns` it is the
4619
+ * header alone. A value that is not a string, number, boolean or null throws a
4620
+ * TypeError: map each row to flat columns first.
4621
+ */
4622
+ declare function toCsv(rows: readonly Readonly<Record<string, CsvValue>>[], options?: {
4623
+ columns?: readonly string[];
4624
+ }): string;
4625
+
4626
+ export { type AnswerSource, type AnthropicOptions, AnthropicProvider, BV_SALA_VERSION, type BivelioHandoff, CATALOGO, COMPACT_ROWS_INSTRUCTION, type CacheOutcome, type CacheTier, type CacheWriteReport, 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, type CsvValue, DEFAULT_COST_MODEL, DEFAULT_MAX_STALENESS_SECONDS, type DeferredExecutionReport, type DeterministicResolver, EMBEDDED_PUBLIC_KEY, type EmbeddingFn, type EvidenceDoc, type ExactCache, ExactTokenizer, type ExecutionClass, ExecutionRouter, type FocusExportOptions, type FocusRow, type FormatChoice, FormatSelector, type FormatSelectorOptions, type HandoffFact, HeuristicEstimator, InMemoryExactCache, InMemorySemanticCache, InMemoryToolSetFreezer, type JsonSchema, type LangfuseGenerationUpdate, LicenseError, type LicenseGate, type LicenseKeyShape, 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 PriceLeverSaving, type PriceLeverSettings, type ProtectedSpan, type Provider, type ProviderRequest, type ProviderResponse, type ProviderStreamEvent, type ProviderStreamOptions, type ProviderUsage, QUALITY_STATUSES, type QualityCacheMatch, type QualityCascadeCheck, type QualityLedger, type QualityRepairCheck, type QualitySchemaCheck, type QualityStatus, type QualityTruncationCheck, REFERENCE_MAX_AGE_MS, REFERENCE_MESSAGE_ALLOWANCE, REFERENCE_OUTPUT_SHARE, type ReferenceCredit, type ReferenceRejection, 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 SavingsAdapterLevers, type SavingsAdapterOptions, type SavingsAttributeOptions, type SavingsCallOptions, type SavingsLayer, type SavingsLayerConfig, SavingsLedger, type SavingsRecord, type SavingsReport, type SavingsReportForExport, type SavingsRequest, type SavingsResult, type SavingsSeriesOptions, type SavingsSeriesRow, type SavingsSpanAttributes, type SavingsSpanOptions, type SavingsStream, type SavingsStreamEvent, type SavingsStreamOptions, type SavingsToggles, type SavingsTracingOptions, SchemaRegistry, type SemanticCacheOptions, type SemanticHit, type SemanticRecord, type SemanticVerifier, type SerializationFormat, type Serializer, Singleflight, type SpanAttributeValue, type SpanLike, type SpanStartOptionsLike, StreamAbortedError, type StreamingReport, type TokenEstimator, TokenizerRegistry, type ToolCallRequest, type ToolDescriptor, type ToolRisk, type ToolSelection, ToolSelector, type ToolSelectorOptions, type ToolSetFreezer, TraceCollector, type TraceEvent, type TracerLike, type TracerProviderLike, UMBRAL_LOCAL, UsageEmitter, type UsageEmitterOptions, type ValidationError, type ValidationResult, type VerificationCheck, type VerificationExclusion, type VerificationOutcome, type VerificationReport, type VerificationSettings, type VerificationSide, type VerificationStratum, adaptiveK, arithmeticResolver, buildExactKey, buildInflightKey, canonicalize, compactRowsSerializer, compresionFiel, computeNextDelay, cosine, createCompactRowsSerializer, createDefaultResolvers, createHandoff, createMockProvider, createSavingsLayer, csvSerializer, decodeCompactRows, dedupe, defaultTokenizerRegistry, deriveSeatId, deriveStatus, detectCi, embebedorLocal, emptyState, encodeCompactRows, evalArithmetic, evaluatePolicy, executeRetrievalPlan, gateFromKey, heuristicEstimator, instrumentSavingsLayer, isKeyset, isStrictCompatible, jsonCompactSerializer, keyTokensMatch, licenseKeyShape, liveGateFromKey, mergeDelta, modelFamilyOf, multisetEqual, negationCount, noopCompressor, normalizeLicenseKey, normalizeText, numericTokens, precioDeLista, projectContext, providerNativeBackend, quitarPrefijoDeProveedor, randomId, recordSavingsOnSpan, registerExactTokenizer, registrarTokenizadorExacto, renderState, resolveConstraintBackend, resolveDependencies, resolveLicense, savingsSpanAttributes, sha256, shouldCompress, stableStringify, toCsv, toFocusRows, toLangfuseGeneration, toSavingsSeries, uniformColumns, validateAgainstSchema, vectorLocal, verifyCompactSignature, verifyLicenseToken, verifyRevocationList, withProtectedSpans, words };