@bivelio/savings-layer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2005 @@
1
+ /**
2
+ * Environment-agnostic UUID generation.
3
+ * ---------------------------------------------------------------------------
4
+ * Trace IDs only need to be unique, not cryptographically unpredictable. We
5
+ * prefer the platform `crypto.randomUUID` (present in modern Node, browsers,
6
+ * Deno and edge runtimes) and fall back to a v4-shaped id when it is absent, so
7
+ * BV-SALA runs everywhere without a `node:crypto` import that breaks bundlers.
8
+ */
9
+ declare function randomId(): string;
10
+
11
+ /**
12
+ * The four-ledger savings accounting.
13
+ *
14
+ * These ledgers are kept strictly separate on purpose (see contracts/types.ts).
15
+ * The ledger is a pure accumulator: the pipeline records events into it, and it
16
+ * produces the numbers that populate the SavingsReport. It never guesses — every
17
+ * increment is driven by an explicit pipeline decision.
18
+ */
19
+ /**
20
+ * De dónde sale el baseline con el que se compara el coste.
21
+ *
22
+ * - `measured` — reconstruido desde el contador del proveedor. Es dinero.
23
+ * - `estimated` — llamada EVITADA: nunca se ejecutó, así que no hay contador
24
+ * del proveedor y el baseline es del estimador local. Sigue
25
+ * siendo la mejor cifra disponible, pero no está medida.
26
+ * - `unmeasured` — el proveedor no reportó uso. No se reclama nada.
27
+ *
28
+ * Sumar `measured` y `estimated` en una misma cifra de "dinero ahorrado" es
29
+ * exactamente lo que un producto verify-don't-trust no puede hacer.
30
+ */
31
+ type CostBasis = "measured" | "estimated" | "unmeasured";
32
+ interface CostModel {
33
+ /** Cost per 1K input tokens, in the caller's currency unit. */
34
+ inputPer1k: number;
35
+ /** Cost per 1K output tokens. */
36
+ outputPer1k: number;
37
+ /** Optional discounted rate for provider-cached (prefix) input tokens. */
38
+ cachedInputPer1k?: number;
39
+ }
40
+ declare class SavingsLedger {
41
+ avoidedLlm: number;
42
+ baselineLlm: number;
43
+ actualLlm: number;
44
+ baselineRetrieval: number;
45
+ actualRetrieval: number;
46
+ baselineTools: number;
47
+ actualTools: number;
48
+ retries: number;
49
+ baselineInput: number;
50
+ actualInput: number;
51
+ eliminatedInput: number;
52
+ compressedInput: number;
53
+ baselineOutput: number;
54
+ actualOutput: number;
55
+ eliminatedOutput: number;
56
+ providerCachedInput: number;
57
+ /** Cuenta LOCAL del prompt que de verdad se envió al proveedor. */
58
+ sentInputLocal: number;
59
+ /** Tokens que la capa quitó de ese prompt, en cuenta LOCAL. */
60
+ removedInputLocal: number;
61
+ /**
62
+ * ¿El estimador que produjo las dos cifras anteriores cuenta con la regla del
63
+ * proveedor? Lo fija el pipeline al resolver el tokenizador del modelo.
64
+ * Por defecto NO: asumir exactitud sin comprobarla es el fallo que esto cierra.
65
+ */
66
+ estimatorExact: boolean;
67
+ /** Salida del reintento de reparación. Es coste NUESTRO, nunca del baseline. */
68
+ repairOutput: number;
69
+ /** Ledger 2: tokens removed because they were not needed. */
70
+ eliminate(tokens: number): void;
71
+ /** Ledger 3: delta between an original encoding and a cheaper one. */
72
+ compress(originalTokens: number, compressedTokens: number): void;
73
+ /** Ledger 4: tokens still present but billed/processed as provider-cached. */
74
+ providerCached(tokens: number): void;
75
+ /** Record that a whole primary LLM call was avoided (cache / resolver hit). */
76
+ avoidCall(baselineInput: number, baselineOutput: number): void;
77
+ /**
78
+ * Baseline de salida publicable.
79
+ *
80
+ * En una llamada EVITADA es el de la respuesta que se habría generado. En una
81
+ * que se EJECUTÓ es la salida medida menos la de la reparación — nunca el techo
82
+ * `maxOutputTokens ?? 256`. estimateCost ya lo hacía por dentro, pero el
83
+ * informe publicaba el techo crudo, y el proyector público lo convertía en
84
+ * dólares: se anunciaba un ahorro de salida que nunca existió.
85
+ */
86
+ get publishedBaselineOutput(): number;
87
+ estimateCost(model: CostModel): {
88
+ baselineEstimate: number;
89
+ actual: number;
90
+ netSavings: number;
91
+ basis: CostBasis;
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Core contracts for BV-SALA (bivelio-savings-layer).
97
+ *
98
+ * The layer keeps FOUR separate ledgers of savings. They must never be summed
99
+ * into a single "tokens saved" number, because each one means something
100
+ * different:
101
+ *
102
+ * - avoided_calls LLM / RAG / tool calls that never executed.
103
+ * - eliminated_tokens information dropped because it was not needed.
104
+ * - compressed_tokens information kept, but encoded with fewer tokens.
105
+ * - provider_cached_tokens tokens still in the prompt that the provider may
106
+ * bill or process differently (prefix/KV caching).
107
+ *
108
+ * A cache hit eliminates a whole call. TOON re-encodes a structured payload.
109
+ * LLMLingua removes redundant words. A provider prefix cache makes tokens
110
+ * cheaper without removing them. Mixing these produces misleading metrics.
111
+ */
112
+ /** Roles understood by the provider-neutral chat interface. */
113
+ type MessageRole = "system" | "user" | "assistant" | "tool";
114
+ interface ChatMessage {
115
+ role: MessageRole;
116
+ content: string;
117
+ /** Optional name (e.g. tool name for role: "tool"). */
118
+ name?: string;
119
+ /** Opaque provider metadata; never used for the exact-cache key. */
120
+ metadata?: Record<string, unknown>;
121
+ }
122
+ /** How a request is classified by the execution router. */
123
+ type ExecutionClass = "NO_RETRIEVAL" | "SINGLE_RETRIEVAL" | "MULTI_HOP_RETRIEVAL" | "TOOL_ACTION" | "NON_CACHEABLE";
124
+ /** Risk profile of a tool. Write/destructive tools are never cached. */
125
+ type ToolRisk = "read" | "write" | "destructive";
126
+ interface ToolDescriptor {
127
+ id: string;
128
+ /** Human/agent intents this tool can satisfy — used by the tool selector. */
129
+ intents: string[];
130
+ risk: ToolRisk;
131
+ /** JSON-schema-like description of the tool's arguments. */
132
+ parameters?: JsonSchema;
133
+ description?: string;
134
+ /** Whether the tool's result is stable enough to cache. */
135
+ freshness?: "static" | "dynamic";
136
+ /** Estimated schema token cost per tokenizer id (optional, filled lazily). */
137
+ schemaTokens?: Record<string, number>;
138
+ }
139
+ /** Minimal structural JSON-Schema subset the layer reasons about. */
140
+ interface JsonSchema {
141
+ type?: "object" | "array" | "string" | "number" | "integer" | "boolean" | "null";
142
+ properties?: Record<string, JsonSchema>;
143
+ required?: string[];
144
+ items?: JsonSchema;
145
+ enum?: unknown[];
146
+ additionalProperties?: boolean | JsonSchema;
147
+ maxItems?: number;
148
+ minItems?: number;
149
+ maxLength?: number;
150
+ minLength?: number;
151
+ maximum?: number;
152
+ minimum?: number;
153
+ pattern?: string;
154
+ format?: string;
155
+ description?: string;
156
+ /** Non-standard: opaque identifier used to reference a registered contract. */
157
+ $id?: string;
158
+ }
159
+ interface OutputContract {
160
+ /** Stable identifier, e.g. "research-answer:v3". */
161
+ contractId: string;
162
+ schema: JsonSchema;
163
+ /** Hard ceiling on generated tokens. */
164
+ maxOutputTokens?: number;
165
+ /** Version marker included in the exact-cache key. */
166
+ version?: string;
167
+ }
168
+ /** Serialization formats the format-selector can choose between. */
169
+ type SerializationFormat = "json-compact" | "toon" | "csv";
170
+ interface RetrievalOptions {
171
+ mode: "off" | "auto" | "single" | "multi";
172
+ maxRounds?: number;
173
+ maxDocuments?: number;
174
+ }
175
+ interface SavingsToggles {
176
+ exactCache?: boolean;
177
+ semanticCache?: boolean | "safe-only";
178
+ compression?: boolean | "auto";
179
+ serialization?: boolean | "auto";
180
+ singleflight?: boolean;
181
+ deterministicResolvers?: boolean;
182
+ router?: boolean;
183
+ }
184
+ interface RequestContext {
185
+ tenantId?: string;
186
+ authorizationScope?: string;
187
+ locale?: string;
188
+ /** Epoch marker of the knowledge base; part of cache keys / freshness. */
189
+ knowledgeEpoch?: string;
190
+ /** Session identifier for conversational memory. */
191
+ sessionId?: string;
192
+ /** Bucket describing how fresh the answer must be. */
193
+ freshnessBucket?: "any" | "day" | "hour" | "live";
194
+ safetyPolicyVersion?: string;
195
+ }
196
+ interface SavingsRequest {
197
+ /** Provider-qualified model id, e.g. "openai/gpt-4o-mini". */
198
+ model: string;
199
+ messages: ChatMessage[];
200
+ tools?: ToolDescriptor[];
201
+ response?: OutputContract;
202
+ retrieval?: RetrievalOptions;
203
+ savings?: SavingsToggles;
204
+ context?: RequestContext;
205
+ /** Sampling controls that participate in the exact-cache key. */
206
+ temperature?: number;
207
+ seed?: number;
208
+ /** Tokenizer id to cost against; defaults to a model-derived estimate. */
209
+ tokenizer?: string;
210
+ /** Arbitrary structured payload to be serialized into the prompt. */
211
+ structuredData?: unknown;
212
+ }
213
+ /** Result returned to the caller. */
214
+ interface SavingsResult<T = unknown> {
215
+ requestId: string;
216
+ /** Validated, structured output (when a contract was supplied). */
217
+ data: T;
218
+ /** Raw text output from the provider (or the resolver). */
219
+ text: string;
220
+ /** Locally-rendered, human-facing representation, when produced. */
221
+ rendered?: string;
222
+ report: SavingsReport;
223
+ }
224
+ type CacheOutcome = "miss" | "exact" | "semantic" | "singleflight" | "deterministic";
225
+ interface SavingsReport {
226
+ requestId: string;
227
+ model: string;
228
+ calls: {
229
+ baselineLlm: number;
230
+ actualLlm: number;
231
+ avoidedLlm: number;
232
+ baselineRetrieval: number;
233
+ actualRetrieval: number;
234
+ baselineTools: number;
235
+ actualTools: number;
236
+ retries: number;
237
+ };
238
+ tokens: {
239
+ baselineInput: number;
240
+ actualInput: number;
241
+ /** Ledger 2: dropped because unnecessary. */
242
+ eliminatedInput: number;
243
+ /** Ledger 3: kept but re-encoded with fewer tokens. */
244
+ compressedInput: number;
245
+ baselineOutput: number;
246
+ actualOutput: number;
247
+ eliminatedOutput: number;
248
+ /** Ledger 4: still present, but provider-cached (cheaper, not removed). */
249
+ providerCachedInput?: number;
250
+ };
251
+ cache: {
252
+ result: CacheOutcome;
253
+ confidence?: number;
254
+ ageMs?: number;
255
+ };
256
+ routing: {
257
+ executionClass: ExecutionClass;
258
+ confidence: number;
259
+ rounds: number;
260
+ reason?: string;
261
+ };
262
+ serialization: {
263
+ chosen: SerializationFormat;
264
+ candidateTokens: Partial<Record<SerializationFormat, number>>;
265
+ instructionOverheadTokens: number;
266
+ };
267
+ compression?: {
268
+ originalTokens: number;
269
+ compressedTokens: number;
270
+ protectedSpans: number;
271
+ fidelityPassed: boolean;
272
+ };
273
+ constraints: {
274
+ backend: string;
275
+ schemaVersion: string;
276
+ structurallyValid: boolean;
277
+ };
278
+ quality: {
279
+ taskPassed?: boolean;
280
+ semanticScore?: number;
281
+ freshnessPassed?: boolean;
282
+ };
283
+ cost: {
284
+ baselineEstimate: number;
285
+ actual: number;
286
+ netSavings: number;
287
+ /**
288
+ * De dónde sale el baseline. Es lo que separa el dinero medido del estimado.
289
+ *
290
+ * - `measured` — reconstruido desde el contador del proveedor de ESTA
291
+ * petición. Es dinero, y se puede contrastar con la factura.
292
+ * - `estimated` — la llamada se EVITÓ, así que nunca hubo contador del
293
+ * proveedor y el baseline sale del estimador local. Es la
294
+ * mejor cifra disponible, pero no está medida.
295
+ * - `unmeasured` — el proveedor no reportó uso. No se reclama nada.
296
+ *
297
+ * Un panel que sume `measured` y `estimated` en una sola cifra de "dinero
298
+ * ahorrado" está haciendo justo lo que este producto promete no hacer.
299
+ */
300
+ basis: CostBasis;
301
+ };
302
+ latencyMs: {
303
+ total: number;
304
+ preprocessing: number;
305
+ provider: number;
306
+ };
307
+ /** Ordered pipeline decision breadcrumbs, for debugging and audit. */
308
+ traces: TraceEvent[];
309
+ }
310
+ /**
311
+ * Structural license gate the pipeline consults before enabling a managed
312
+ * feature (semantic cache, learned thresholds, long-context compression, …).
313
+ *
314
+ * It is intentionally a bare shape — the SDK never imports the license package.
315
+ * `@bivelio/license-client`'s `createLicenseGate()` returns a compatible object,
316
+ * but any implementation of this interface works. When no gate is configured the
317
+ * pipeline behaves as an ungated self-managed deployment (backwards compatible).
318
+ */
319
+ interface LicenseGate {
320
+ isEntitled(feature: string): boolean;
321
+ /**
322
+ * Whether there is an overall active (or in-grace) license. La consulta la
323
+ * puerta de entrada fail-closed del pipeline.
324
+ *
325
+ * OBLIGATORIO. Fue opcional "por compatibilidad", y esa opcionalidad era el
326
+ * bypass: un gate al que le faltara el método dejaba pasar todas las
327
+ * peticiones con solo un aviso por consola. Para desarrollo local está
328
+ * `devUnlicensed: true`, que es explícito y no se cuela sin querer.
329
+ */
330
+ isActive(): boolean;
331
+ /** Resolved status (active|grace|expired|invalid|revoked), for error messages. */
332
+ status?(): string;
333
+ }
334
+ interface TraceEvent {
335
+ stage: string;
336
+ outcome: string;
337
+ detail?: Record<string, unknown>;
338
+ /** Monotonic elapsed ms from the start of the request. */
339
+ atMs: number;
340
+ }
341
+
342
+ /**
343
+ * Typed intermediate representation for LLM -> LLM handoffs.
344
+ *
345
+ * In multi-agent systems the same system prompt, tool list and context tend to
346
+ * be re-explained in free-form natural language at every hop. BV-SALA replaces
347
+ * that with a compact, typed handoff object. Each consumer receives a
348
+ * projection of the state, not the whole conversation. The serializer then
349
+ * decides whether to send it as compact JSON or TOON.
350
+ */
351
+ interface HandoffFact {
352
+ id: string;
353
+ value: unknown;
354
+ source?: string;
355
+ }
356
+ interface BivelioHandoff {
357
+ /** Schema version of the handoff envelope itself. */
358
+ v: 1;
359
+ task: string;
360
+ goal: string;
361
+ rules: string[];
362
+ facts: HandoffFact[];
363
+ /** References to large documents kept out of the prompt (id/hash pointers). */
364
+ refs: string[];
365
+ /** Only the delta since the previous turn, not the whole state. */
366
+ delta?: Record<string, unknown>;
367
+ /** Identifier of the output contract; not the full schema. */
368
+ outputContract: string;
369
+ }
370
+ declare function createHandoff(init: Omit<BivelioHandoff, "v"> & Partial<Pick<BivelioHandoff, "v">>): BivelioHandoff;
371
+
372
+ /**
373
+ * Token estimation.
374
+ *
375
+ * A production deployment should register a real tokenizer (tiktoken /
376
+ * o200k_base, the provider's own tokenizer, etc.) via {@link TokenizerRegistry}.
377
+ * BV-SALA never hard-codes savings; every format / compression decision is made
378
+ * against the tokenizer of the *destination* model. This module ships a
379
+ * dependency-free heuristic estimator so the layer works out of the box and so
380
+ * tests are deterministic — but it is explicitly an *estimate*, and the whole
381
+ * point of the registry is that you can swap in an exact counter.
382
+ */
383
+ interface TokenEstimator {
384
+ /** Stable id, e.g. "o200k_base" or "heuristic". */
385
+ readonly id: string;
386
+ /**
387
+ * ¿Cuenta este estimador con la MISMA regla que factura el proveedor?
388
+ *
389
+ * No es un detalle de calidad: decide si una cifra derivada puede llamarse
390
+ * MEDIDA. El baseline de una llamada que se ejecutó se reconstruye como
391
+ * "lo enviado (medido por el proveedor) + lo quitado (contado aquí)". Si lo
392
+ * quitado se cuenta con otra regla, el resultado es una estimación aunque uno
393
+ * de los dos sumandos venga del proveedor — y publicarlo como medido es
394
+ * exactamente la clase de mentira que este producto existe para no contar.
395
+ *
396
+ * El heurístico vale para RANKEAR formatos candidatos (le basta con acertar el
397
+ * orden). No vale para restar tokens entre formatos distintos: cobra cada
398
+ * símbolo suelto como un token entero, así que infla el JSON denso en símbolos
399
+ * mucho más que el TOON, y la diferencia se acredita como ahorro inexistente.
400
+ */
401
+ readonly exact: boolean;
402
+ /** Estimate the number of tokens in a string. */
403
+ count(text: string): number;
404
+ }
405
+ /**
406
+ * Heuristic estimator. Modern BPE tokenizers (cl100k/o200k) average roughly
407
+ * ~3.7-4 characters per token on English/code, with whitespace, digits and
408
+ * punctuation costing relatively more. This estimator approximates that by
409
+ * combining a word-boundary pass with a character-based floor, which tracks
410
+ * real tokenizers well enough to *rank* candidate encodings — which is all the
411
+ * format selector needs. It is intentionally conservative and stable.
412
+ */
413
+ declare class HeuristicEstimator implements TokenEstimator {
414
+ readonly id: string;
415
+ /** Aproxima; no comparte regla con ningún proveedor. Ver {@link TokenEstimator.exact}. */
416
+ readonly exact = false;
417
+ constructor(id?: string);
418
+ count(text: string): number;
419
+ }
420
+ declare const heuristicEstimator: HeuristicEstimator;
421
+
422
+ /**
423
+ * Registry of token estimators keyed by tokenizer id, plus a mapping from model
424
+ * ids to the tokenizer they use. All cost/format/compression decisions in the
425
+ * pipeline resolve a tokenizer through this registry, so swapping in an exact
426
+ * tokenizer is a one-line registration and never a code change in the pipeline.
427
+ */
428
+ declare class TokenizerRegistry {
429
+ private readonly estimators;
430
+ private readonly modelToTokenizer;
431
+ private readonly fallbackId;
432
+ constructor(fallback?: TokenEstimator);
433
+ register(estimator: TokenEstimator): this;
434
+ /** Associate a model id (e.g. "openai/gpt-4o-mini") with a tokenizer id. */
435
+ mapModel(model: string, tokenizerId: string): this;
436
+ /** Resolve an estimator by explicit tokenizer id, else by model, else fallback. */
437
+ resolve(opts: {
438
+ tokenizer?: string;
439
+ model?: string;
440
+ }): TokenEstimator;
441
+ count(text: string, opts: {
442
+ tokenizer?: string;
443
+ model?: string;
444
+ }): number;
445
+ }
446
+ declare const defaultTokenizerRegistry: TokenizerRegistry;
447
+
448
+ /**
449
+ * Tokenizador EXACTO — la regla del proveedor, no una aproximación.
450
+ *
451
+ * Por qué existe. El baseline de una llamada que se ejecutó se reconstruye como
452
+ * "lo enviado + lo quitado". Lo enviado lo mide el proveedor; lo quitado lo
453
+ * contamos aquí. Con el estimador heurístico esos dos sumandos vienen de reglas
454
+ * distintas, y la diferencia NO es ruido: el heurístico cobra cada símbolo suelto
455
+ * como un token entero, así que infla el JSON (40% de símbolos) mucho más que el
456
+ * TOON (16%). Medido en el escenario de la tabla: publicaba un baseline 3,68x el
457
+ * naive real, o sea un +47,4% de ahorro donde el proveedor midió −49,6%.
458
+ *
459
+ * Cómo se instala. `gpt-tokenizer` son ~53 MB desempaquetados, así que NO es
460
+ * dependencia dura: imponérsela a todo el que instale el SDK, cuando la mayoría
461
+ * no necesita contabilidad medida, sería abusivo. Es peerDependency opcional:
462
+ *
463
+ * pnpm add gpt-tokenizer
464
+ *
465
+ * Sin ella el layer sigue funcionando con el heurístico, y el `basis` de cada
466
+ * informe lo dice — "estimated" en vez de "measured". Esa es la diferencia
467
+ * importante: el producto no deja de funcionar, deja de AFIRMAR de más.
468
+ */
469
+
470
+ /** Lo que necesitamos de gpt-tokenizer. Tipado aquí para no depender de sus tipos. */
471
+ interface CodificadorBpe {
472
+ encode(text: string): number[] | {
473
+ length: number;
474
+ };
475
+ }
476
+ /**
477
+ * Estimador exacto sobre una codificación BPE ya cargada.
478
+ *
479
+ * Se construye con el codificador ya resuelto en vez de importarlo dentro: así
480
+ * este módulo no obliga a nadie a tener el paquete, y quien lo tiene decide qué
481
+ * codificación carga (o200k_base para gpt-4o/4.1/5, cl100k_base para 3.5/4).
482
+ */
483
+ declare class ExactTokenizer implements TokenEstimator {
484
+ private readonly codificador;
485
+ readonly id: string;
486
+ readonly exact = true;
487
+ constructor(id: string, codificador: CodificadorBpe);
488
+ count(text: string): number;
489
+ }
490
+ /** Modelos que usan cada codificación. Sirve para mapear el registro de una vez. */
491
+ declare const MODELOS_POR_CODIFICACION: Readonly<Record<string, readonly string[]>>;
492
+ /**
493
+ * Carga `gpt-tokenizer` si está instalado y registra el tokenizador exacto para
494
+ * los modelos de OpenAI. Devuelve `false` —sin lanzar— cuando no está: la
495
+ * ausencia del paquete opcional no es un error, solo significa que la
496
+ * contabilidad se publicará como estimada.
497
+ *
498
+ * Se importa solo la codificación pedida, no el paquete entero: `gpt-tokenizer`
499
+ * expone un punto de entrada por codificación y cargarlas todas serían decenas
500
+ * de MB en memoria sin motivo.
501
+ */
502
+ declare function registrarTokenizadorExacto(registro: {
503
+ register(e: TokenEstimator): unknown;
504
+ mapModel(model: string, tokenizerId: string): unknown;
505
+ }, opciones?: {
506
+ codificaciones?: readonly string[];
507
+ }): Promise<boolean>;
508
+
509
+ /**
510
+ * Canonicalization turns a raw request into a normalized, hashable form. This
511
+ * is what makes the exact cache safe and the singleflight key correct: two
512
+ * requests that mean the same thing must canonicalize identically, and two
513
+ * requests that differ in any way that could change the answer must not.
514
+ */
515
+ interface CanonicalRequest {
516
+ model: string;
517
+ modelFamily: string;
518
+ messages: ChatMessage[];
519
+ /** Whitespace/case-normalized text of the user-visible turns. */
520
+ normalizedText: string;
521
+ systemPromptHash: string;
522
+ toolsetHash: string;
523
+ /** Hash of the structured payload; part of the cache key so different data never collides. */
524
+ structuredDataHash: string;
525
+ schemaVersion: string;
526
+ contractId: string | null;
527
+ temperature: number | null;
528
+ seed: number | null;
529
+ tenantId: string;
530
+ authorizationScope: string;
531
+ /**
532
+ * Quién pregunta, dentro del tenant. Sale de `context.sessionId`.
533
+ *
534
+ * Es lo que permite cachear una respuesta PERSONALIZADA sin servírsela a otro:
535
+ * tenant y scope son compartidos por defecto, así que sin esto dos usuarios de
536
+ * la misma organización comparten entrada de caché. `null` cuando no se
537
+ * aporta, y entonces la política apaga la caché exacta para ese tráfico.
538
+ */
539
+ principal: string | null;
540
+ locale: string;
541
+ knowledgeEpoch: string;
542
+ freshnessBucket: string;
543
+ safetyPolicyVersion: string;
544
+ tokenizer: string | undefined;
545
+ original: SavingsRequest;
546
+ }
547
+ declare function normalizeText(text: string): string;
548
+ declare function sha256(input: string): string;
549
+ /** Stable JSON stringify with sorted keys so hashes are order-independent. */
550
+ declare function stableStringify(value: unknown): string;
551
+ declare function canonicalize(request: SavingsRequest): CanonicalRequest;
552
+
553
+ /**
554
+ * Exact-cache key.
555
+ *
556
+ * A hash of the user text alone is NOT a safe cache key. Two requests may share
557
+ * text but differ in tenant, permission scope, model version, system prompt,
558
+ * schema, toolset, locale, sampling, knowledge epoch or safety policy — any of
559
+ * which can change the correct answer. The exact key includes all of them.
560
+ */
561
+ declare function buildExactKey(c: CanonicalRequest): string;
562
+ /**
563
+ * In-flight (singleflight) key. Coalesces concurrent, equivalent requests onto
564
+ * a single execution. It can be slightly looser than the exact key (it does not
565
+ * need freshness bucketing, since the work is happening *now*), but must still
566
+ * isolate tenants and permissions.
567
+ */
568
+ declare function buildInflightKey(c: CanonicalRequest): string;
569
+
570
+ /**
571
+ * Policy engine: risk and freshness.
572
+ *
573
+ * This is the gate that decides which savings are *safe* for a given request.
574
+ * The most dangerous failure of a savings layer is not a cache miss — it is
575
+ * confidently returning a wrong answer (a stale price, another tenant's data, a
576
+ * reused answer for a side-effecting action). The policy engine exists to make
577
+ * those outcomes structurally impossible, not merely unlikely.
578
+ */
579
+ interface Policy {
580
+ /** May the exact cache be read/written for this request? */
581
+ allowExactCache: boolean;
582
+ /** May the semantic cache even be *considered*? (still verified later) */
583
+ allowSemanticCache: boolean;
584
+ /** May concurrent equivalent requests be coalesced? */
585
+ allowSingleflight: boolean;
586
+ /** May we run natural-language compression on the context? */
587
+ allowCompression: boolean;
588
+ /** Does the request demand current/live data? */
589
+ requiresFreshData: boolean;
590
+ /** Does the request have (or request) side effects? */
591
+ hasSideEffects: boolean;
592
+ /** Is the response personalized to a user/account? */
593
+ isPersonalized: boolean;
594
+ reasons: string[];
595
+ }
596
+ declare function evaluatePolicy(c: CanonicalRequest): Policy;
597
+
598
+ /**
599
+ * Exact cache.
600
+ *
601
+ * When the canonical key represents the request completely, a hit removes 100%
602
+ * of input tokens, 100% of output tokens, and the whole provider call. This is
603
+ * the single highest-value, lowest-risk saving in the layer. The interface is
604
+ * deliberately minimal (get/set with TTL) so any store — Redis, a KV, an LRU —
605
+ * can back it.
606
+ */
607
+ interface CachedAnswer<T = unknown> {
608
+ text: string;
609
+ data?: T;
610
+ rendered?: string;
611
+ /** Epoch ms when stored; used to compute age and honor TTL. */
612
+ storedAtMs: number;
613
+ /** Optional metadata carried alongside the answer. */
614
+ meta?: Record<string, unknown>;
615
+ }
616
+ interface ExactCache {
617
+ get<T = unknown>(key: string): Promise<CachedAnswer<T> | null>;
618
+ set<T = unknown>(key: string, value: CachedAnswer<T>, ttlMs?: number): Promise<void>;
619
+ delete(key: string): Promise<void>;
620
+ }
621
+ /**
622
+ * In-memory exact cache with TTL and a size bound (LRU-ish by insertion order).
623
+ * Suitable for tests and single-process deployments; swap for Redis in prod.
624
+ */
625
+ declare class InMemoryExactCache implements ExactCache {
626
+ private readonly store;
627
+ private readonly maxEntries;
628
+ private readonly now;
629
+ constructor(opts?: {
630
+ maxEntries?: number;
631
+ now?: () => number;
632
+ });
633
+ get<T = unknown>(key: string): Promise<CachedAnswer<T> | null>;
634
+ set<T = unknown>(key: string, value: CachedAnswer<T>, ttlMs?: number): Promise<void>;
635
+ delete(key: string): Promise<void>;
636
+ get size(): number;
637
+ }
638
+
639
+ /**
640
+ * Singleflight (in-flight coalescing).
641
+ *
642
+ * When N equivalent requests arrive at once, exactly one executes the work and
643
+ * the other N-1 await the same promise. This is NOT a historical cache — it is
644
+ * de-duplication of concurrent work, so it carries no staleness risk and needs
645
+ * no TTL. On traffic spikes it can eliminate a large fraction of duplicate
646
+ * provider calls for free.
647
+ */
648
+ declare class Singleflight {
649
+ private readonly inflight;
650
+ /** How many callers are currently sharing an in-flight execution. */
651
+ private readonly waiters;
652
+ /**
653
+ * Run `fn` under `key`. If a call with the same key is already running, share
654
+ * its result instead of starting a new one. Returns the shared result plus
655
+ * whether this caller was a coalesced follower (a "hit").
656
+ */
657
+ run<T>(key: string, fn: () => Promise<T>): Promise<{
658
+ value: T;
659
+ coalesced: boolean;
660
+ }>;
661
+ get activeKeys(): number;
662
+ }
663
+
664
+ /**
665
+ * Semantic cache (safe-by-construction).
666
+ *
667
+ * A semantic cache reuses a previous answer for a *similar* query. Its danger is
668
+ * the false positive: confidently returning a valid answer to a nearby-but-wrong
669
+ * question. Therefore the headline metric here is NOT hit ratio — it is the
670
+ * PRECISION of hits. Every candidate must pass: tenant/permission filter,
671
+ * knowledge-epoch filter, a similarity threshold, a stronger local verifier, a
672
+ * freshness check, and an output-contract compatibility check.
673
+ */
674
+ interface EmbeddingFn {
675
+ (text: string): Promise<number[]> | number[];
676
+ }
677
+ interface SemanticVerifier {
678
+ /**
679
+ * Second-stage check (ideally a small cross-encoder). Returns a confidence in
680
+ * [0,1] that the candidate answer genuinely answers the query. The pipeline
681
+ * only accepts a hit above the configured acceptance threshold.
682
+ */
683
+ (query: string, candidateQuery: string): Promise<number> | number;
684
+ }
685
+ interface SemanticRecord {
686
+ embedding: number[];
687
+ query: string;
688
+ answer: CachedAnswer;
689
+ tenantId: string;
690
+ authorizationScope: string;
691
+ knowledgeEpoch: string;
692
+ contractId: string | null;
693
+ /**
694
+ * QUIÉN hizo la petición (sessionId/principal), o null si es anónima.
695
+ *
696
+ * Faltaba, y era la fuga más cara: la caché exacta aísla por principal pero la
697
+ * semántica no lo miraba, así que dos usuarios distintos del MISMO tenant con
698
+ * la misma pregunta casaban por embedding y el segundo recibía la respuesta
699
+ * calculada para el primero. `tenantId` separa organizaciones, no personas.
700
+ */
701
+ principal: string | null;
702
+ /**
703
+ * Identidad COMPLETA de la petición que produjo esta respuesta.
704
+ *
705
+ * Antes solo se guardaban tenant, scope, epoch y contrato: la clave exacta
706
+ * declara necesarios otros cinco campos, y la semántica los ignoraba. El peor
707
+ * era `structuredDataHash` — dos preguntas redactadas igual pero con datos
708
+ * ADJUNTOS distintos casan por embedding, así que se servía la respuesta
709
+ * calculada sobre la tabla de otro. `model` y `systemPromptHash` van detrás:
710
+ * un modelo o un system prompt distintos son otra respuesta.
711
+ */
712
+ model: string;
713
+ systemPromptHash: string;
714
+ structuredDataHash: string;
715
+ locale: string;
716
+ schemaVersion: string;
717
+ safetyPolicyVersion: string;
718
+ storedAtMs: number;
719
+ }
720
+ interface SemanticHit {
721
+ record: SemanticRecord;
722
+ similarity: number;
723
+ verifierScore: number;
724
+ confidence: number;
725
+ ageMs: number;
726
+ }
727
+ interface SemanticCacheOptions {
728
+ embed: EmbeddingFn;
729
+ verify?: SemanticVerifier;
730
+ /** Cosine similarity floor for a candidate to be considered at all. */
731
+ similarityThreshold?: number;
732
+ /** Verifier score floor for acceptance. */
733
+ acceptThreshold?: number;
734
+ ttlMs?: number;
735
+ maxRecords?: number;
736
+ now?: () => number;
737
+ }
738
+ declare function cosine(a: number[], b: number[]): number;
739
+ /** True when two normalized queries agree on the facts an embedding tends to blur. */
740
+ declare function keyTokensMatch(a: string, b: string): boolean;
741
+ /**
742
+ * In-memory reference implementation. A production build would back this with a
743
+ * real vector store (pgvector, Qdrant, etc.) and metadata filters pushed down
744
+ * to the store. The verification logic — the part that makes it *safe* — stays
745
+ * identical regardless of the backend.
746
+ */
747
+ declare class InMemorySemanticCache {
748
+ private readonly records;
749
+ private readonly embed;
750
+ private readonly verify;
751
+ private readonly similarityThreshold;
752
+ private readonly acceptThreshold;
753
+ private readonly ttlMs;
754
+ private readonly maxRecords;
755
+ private readonly now;
756
+ constructor(opts: SemanticCacheOptions);
757
+ /**
758
+ * Find a safe hit for a canonical request, or null. Metadata filters run
759
+ * FIRST and hard — a candidate from another tenant, scope, or knowledge epoch
760
+ * is never even scored.
761
+ */
762
+ find(c: CanonicalRequest): Promise<SemanticHit | null>;
763
+ store(c: CanonicalRequest, answer: CachedAnswer): Promise<void>;
764
+ /** Drop every record for a knowledge epoch (invalidation on re-index). */
765
+ invalidateEpoch(knowledgeEpoch: string): number;
766
+ get size(): number;
767
+ }
768
+
769
+ /**
770
+ * Deterministic resolvers.
771
+ *
772
+ * The cheapest LLM call is the one that never happens because the answer can be
773
+ * *computed* locally. Arithmetic, unit conversions, deterministic string
774
+ * transforms, lookups in a local table — none of these need a model. A resolver
775
+ * inspects the canonical request and either produces an answer or declines.
776
+ * The first resolver to produce a result wins; the pipeline then skips the
777
+ * entire provider path.
778
+ */
779
+ interface ResolverResult {
780
+ text: string;
781
+ data?: unknown;
782
+ /** Which resolver produced this, for the trace. */
783
+ resolver: string;
784
+ }
785
+ interface DeterministicResolver {
786
+ readonly name: string;
787
+ try(c: CanonicalRequest): ResolverResult | null;
788
+ }
789
+ declare class ResolverRegistry {
790
+ private readonly resolvers;
791
+ register(resolver: DeterministicResolver): this;
792
+ try(c: CanonicalRequest): ResolverResult | null;
793
+ get size(): number;
794
+ }
795
+ /**
796
+ * Safe arithmetic resolver: answers "what is 2 + 2 * 3" style questions using a
797
+ * tiny, dependency-free shunting-yard evaluator over +, -, *, /, %, parens and
798
+ * decimals. It never uses eval, and declines anything it does not fully parse.
799
+ */
800
+ declare const arithmeticResolver: DeterministicResolver;
801
+ /** Shunting-yard evaluation of a numeric expression. Returns null on any parse issue. */
802
+ declare function evalArithmetic(expr: string): number | null;
803
+ /** A registry seeded with the built-in safe resolvers. */
804
+ declare function createDefaultResolvers(): ResolverRegistry;
805
+
806
+ /**
807
+ * Execution router (inspired by Adaptive-RAG).
808
+ *
809
+ * The single most impactful decision is often not "how do I compress this
810
+ * prompt" but "does this request need retrieval / tools at all". We do NOT use
811
+ * a large LLM to decide whether to call a large LLM. The router is:
812
+ * deterministic rules + a small local classifier hook + confidence.
813
+ *
814
+ * It is designed to be deployed in SHADOW MODE first: it produces a decision
815
+ * and a confidence, and you compare its choices against realized cost/quality
816
+ * before letting it gate execution.
817
+ *
818
+ * Robustness note: small paraphrases can flip retrieval decisions. The router
819
+ * classifies over the *normalized* text and exposes its reasoning so a
820
+ * paraphrase-stability suite can measure decision drift.
821
+ */
822
+ interface RouteDecision {
823
+ executionClass: ExecutionClass;
824
+ confidence: number;
825
+ reason: string;
826
+ /** Suggested number of retrieval rounds (0 for NO_RETRIEVAL). */
827
+ rounds: number;
828
+ }
829
+ /** Optional local classifier that can override/augment the rules. */
830
+ interface LocalClassifier {
831
+ classify(text: string): {
832
+ executionClass: ExecutionClass;
833
+ confidence: number;
834
+ } | null;
835
+ }
836
+ declare class ExecutionRouter {
837
+ private readonly classifier;
838
+ private readonly maxRounds;
839
+ constructor(opts?: {
840
+ classifier?: LocalClassifier;
841
+ maxRounds?: number;
842
+ });
843
+ classify(c: CanonicalRequest): RouteDecision;
844
+ }
845
+
846
+ /**
847
+ * Tool selector.
848
+ *
849
+ * Full tool schemas can dominate a prompt even when most tools are irrelevant.
850
+ * The selector picks only the tools whose declared intents match the request,
851
+ * and — critically — gates write/destructive tools behind explicit rules so a
852
+ * read-only request can never be handed a mutating capability.
853
+ */
854
+ interface ToolSelection {
855
+ tools: ToolDescriptor[];
856
+ droppedIds: string[];
857
+ reason: string;
858
+ }
859
+ interface ToolSelectorOptions {
860
+ /** Max tools to include (schema-token budget guard). */
861
+ maxTools?: number;
862
+ /** Tokenizer-aware scorer for tie-breaking (optional). */
863
+ scoreIntentMatch?: (text: string, tool: ToolDescriptor) => number;
864
+ }
865
+ declare class ToolSelector {
866
+ private readonly maxTools;
867
+ private readonly scoreIntentMatch;
868
+ constructor(opts?: ToolSelectorOptions);
869
+ select(text: string, tools: ToolDescriptor[] | undefined, execution: ExecutionClass): ToolSelection;
870
+ }
871
+
872
+ /**
873
+ * Retrieval planning: retrieve -> dedupe -> rerank -> adaptive-k.
874
+ *
875
+ * Eliminating irrelevant context is cheaper and safer than compressing it, so
876
+ * retrieval prunes aggressively BEFORE anything reaches the prompt. Documents
877
+ * are kept out of the prompt where possible and passed as references/hashes;
878
+ * only the selected, trimmed evidence is projected in.
879
+ */
880
+ interface EvidenceDoc {
881
+ id: string;
882
+ text: string;
883
+ /** Optional pre-computed relevance score from the retriever. */
884
+ score?: number;
885
+ source?: string;
886
+ /** Content hash, so large docs can be referenced instead of inlined. */
887
+ hash?: string;
888
+ }
889
+ interface Retriever {
890
+ retrieve(query: string, k: number): Promise<EvidenceDoc[]>;
891
+ }
892
+ interface Reranker {
893
+ rerank(query: string, docs: EvidenceDoc[]): Promise<EvidenceDoc[]>;
894
+ }
895
+ interface RetrievalPlan {
896
+ execution: ExecutionClass;
897
+ query: string;
898
+ maxRounds: number;
899
+ maxDocuments: number;
900
+ }
901
+ interface RetrievalOutcome {
902
+ docs: EvidenceDoc[];
903
+ rounds: number;
904
+ retrievedCount: number;
905
+ keptCount: number;
906
+ }
907
+ /** Remove exact and near-duplicate docs by normalized text / hash. */
908
+ declare function dedupe(docs: EvidenceDoc[]): EvidenceDoc[];
909
+ /**
910
+ * Adaptive-k: choose how many documents to keep based on the score distribution
911
+ * rather than a fixed cutoff. When the top document dominates, keep fewer; when
912
+ * scores are flat, keep up to the cap. This directly trims input tokens.
913
+ */
914
+ declare function adaptiveK(docs: EvidenceDoc[], opts?: {
915
+ maxDocuments: number;
916
+ minDocuments?: number;
917
+ }): EvidenceDoc[];
918
+ /**
919
+ * Execute a retrieval plan. NO_RETRIEVAL / TOOL_ACTION classes retrieve nothing.
920
+ * Otherwise: retrieve -> dedupe -> rerank -> adaptive-k, bounded by maxRounds.
921
+ */
922
+ declare function executeRetrievalPlan(plan: RetrievalPlan, deps: {
923
+ retriever?: Retriever;
924
+ reranker?: Reranker;
925
+ }): Promise<RetrievalOutcome>;
926
+
927
+ /**
928
+ * Conversational memory by deltas.
929
+ *
930
+ * A major source of wasted input tokens is resending the entire chat history
931
+ * every turn. Instead we keep a compact structured state and, each turn, apply a
932
+ * small delta produced BY THE MAIN GENERATION (not a separate summarization
933
+ * call). The next prompt receives: compact relevant state + the last 1-3 raw
934
+ * turns + the current message — never the whole conversation.
935
+ */
936
+ interface ConversationState {
937
+ goals: string[];
938
+ confirmedFacts: Record<string, unknown>;
939
+ userPreferences: Record<string, unknown>;
940
+ decisions: string[];
941
+ openItems: string[];
942
+ evidenceRefs: string[];
943
+ }
944
+ interface MemoryDelta {
945
+ set?: Record<string, unknown>;
946
+ append?: Record<string, unknown[]>;
947
+ remove?: string[];
948
+ }
949
+ declare function emptyState(): ConversationState;
950
+ /**
951
+ * Merge a delta into state locally. `set` updates confirmedFacts/preferences,
952
+ * `append` pushes onto list fields, `remove` deletes fact keys or list entries.
953
+ * The merge is deterministic and side-effect-free (returns a new object).
954
+ */
955
+ declare function mergeDelta(state: ConversationState, delta: MemoryDelta): ConversationState;
956
+ /**
957
+ * Project the state + recent turns into the minimal message set to send. Only
958
+ * relevant, non-empty state is serialized, then the last `rawTurns` messages,
959
+ * then the current message.
960
+ */
961
+ declare function projectContext(opts: {
962
+ state: ConversationState;
963
+ history: ChatMessage[];
964
+ current: ChatMessage;
965
+ rawTurns?: number;
966
+ }): ChatMessage[];
967
+ declare function renderState(state: ConversationState): string;
968
+
969
+ /**
970
+ * A serializer encodes a structured payload into prompt text, and reports the
971
+ * instruction overhead a model needs to *understand* that encoding. The format
972
+ * selector compares `tokens(encode) + tokens(instruction)` across serializers.
973
+ */
974
+ interface Serializer {
975
+ readonly format: SerializationFormat;
976
+ /** Whether this serializer can represent the given value at all. */
977
+ supports(value: unknown): boolean;
978
+ encode(value: unknown): string;
979
+ /**
980
+ * The instruction text a model needs to interpret this format. For JSON this
981
+ * is empty; for TOON/CSV it is the per-request cost that must be amortized
982
+ * before the format is worthwhile.
983
+ */
984
+ instruction(value: unknown): string;
985
+ /**
986
+ * Decode encoded text back to a value, used ONLY for the selector's round-trip
987
+ * fidelity check. A serializer without a faithful decoder is treated as
988
+ * UNVERIFIABLE: the selector will not choose it (fail-closed), so we never ship
989
+ * — or bill savings for — an encoding we can't prove reproduces the payload.
990
+ * JSON-compact is inherently lossless and is exempt from the check.
991
+ */
992
+ decode?(text: string): unknown;
993
+ }
994
+
995
+ /**
996
+ * Compact JSON: the lossless baseline every other format is measured against.
997
+ * No indentation, no incidental whitespace. This is frequently the *winner* for
998
+ * deeply nested or non-uniform data, which is exactly why it is the baseline and
999
+ * TOON is only ever a challenger.
1000
+ */
1001
+ declare const jsonCompactSerializer: Serializer;
1002
+
1003
+ /**
1004
+ * TOON (Token-Oriented Object Notation) adapter.
1005
+ *
1006
+ * TOON's strength is UNIFORM ARRAYS OF FLAT OBJECTS: it declares the column
1007
+ * names once and then emits one row per object, which removes the repeated keys
1008
+ * that make JSON expensive for tabular data. On deeply nested or non-uniform
1009
+ * data TOON can be *larger* than compact JSON — so this is only ever a candidate
1010
+ * the selector may pick, never a default.
1011
+ *
1012
+ * This is an in-house, pinned encoder placed behind the {@link Serializer}
1013
+ * interface (as recommended: do not couple the system to an upstream `main`).
1014
+ * It emits a well-defined subset and ships a matching decoder so the JSON<->TOON
1015
+ * round-trip is deterministic and testable.
1016
+ */
1017
+ interface ToonOptions {
1018
+ /** Field delimiter within tabular/inline rows. */
1019
+ delimiter?: "," | "\t";
1020
+ /** Indentation unit. */
1021
+ indent?: string;
1022
+ }
1023
+ declare function encodeToon(value: unknown, options?: ToonOptions): string;
1024
+ /** Return shared column names iff every element is a flat, scalar-valued object with identical keys. */
1025
+ declare function uniformColumns(arr: unknown[]): string[] | null;
1026
+ /**
1027
+ * The instruction a model needs to read TOON. Kept short on purpose — this text
1028
+ * is the per-request overhead the selector must amortize before TOON wins.
1029
+ */
1030
+ declare const TOON_INSTRUCTION: string;
1031
+ declare function createToonSerializer(options?: ToonOptions): Serializer;
1032
+ declare const toonSerializer: Serializer;
1033
+
1034
+ /**
1035
+ * TOON decoder for the subset emitted by {@link encodeToon}.
1036
+ *
1037
+ * Supports: scalar fields, nested objects, empty arrays, inline scalar arrays
1038
+ * (`key[N]: a,b,c`) and tabular object arrays (`key[N]{c1,c2}:` + rows). This
1039
+ * covers the cases the format selector actually chooses TOON for, and makes the
1040
+ * JSON<->TOON round-trip deterministic and testable. List-form arrays of nested
1041
+ * objects are intentionally out of scope for decoding (the selector never picks
1042
+ * TOON for those, since JSON wins there anyway).
1043
+ */
1044
+ interface ToonDecodeOptions {
1045
+ delimiter?: "," | "\t";
1046
+ indent?: string;
1047
+ }
1048
+ declare function decodeToon(text: string, options?: ToonDecodeOptions): unknown;
1049
+
1050
+ /**
1051
+ * CSV serializer — only valid for genuinely tabular data (a uniform array of
1052
+ * flat objects). CSV can be marginally smaller than TOON on pure tables, but it
1053
+ * does NOT preserve the JSON data model in general, so `supports()` is strict.
1054
+ */
1055
+ declare const csvSerializer: Serializer;
1056
+
1057
+ /**
1058
+ * Format selector.
1059
+ *
1060
+ * TOON is a candidate, never a default. For each request we count, USING THE
1061
+ * DESTINATION MODEL'S TOKENIZER:
1062
+ *
1063
+ * tokens(encoded payload) + tokens(instruction to understand it)
1064
+ *
1065
+ * ...and pick the cheapest, subject to activation thresholds so we do not switch
1066
+ * formats to save a trivial amount (which risks quality for no real benefit).
1067
+ * JSON-compact is the baseline and the fallback; a format only wins if it beats
1068
+ * JSON by both an absolute and a relative margin.
1069
+ */
1070
+ interface FormatSelectorOptions {
1071
+ tokenizer: TokenizerRegistry;
1072
+ serializers?: Serializer[];
1073
+ /** Minimum absolute token saving vs JSON to switch away from JSON. */
1074
+ minimumSavingsTokens?: number;
1075
+ /** Minimum relative saving vs JSON (0..1) to switch away from JSON. */
1076
+ minimumSavingsRatio?: number;
1077
+ /**
1078
+ * Per-(model,format) quality penalty in tokens. Encodes "this model is worse
1079
+ * at reading TOON" as an additive cost, learned from production traces.
1080
+ */
1081
+ qualityPenalty?: (model: string, format: SerializationFormat) => number;
1082
+ }
1083
+ interface FormatChoice {
1084
+ chosen: SerializationFormat;
1085
+ encoded: string;
1086
+ instruction: string;
1087
+ candidateTokens: Partial<Record<SerializationFormat, number>>;
1088
+ instructionOverheadTokens: number;
1089
+ reason: string;
1090
+ }
1091
+ declare class FormatSelector {
1092
+ private readonly tokenizer;
1093
+ private readonly serializers;
1094
+ private readonly minSavingsTokens;
1095
+ private readonly minSavingsRatio;
1096
+ private readonly qualityPenalty;
1097
+ constructor(opts: FormatSelectorOptions);
1098
+ choose(payload: unknown, opts: {
1099
+ model?: string;
1100
+ tokenizer?: string;
1101
+ }): FormatChoice;
1102
+ }
1103
+
1104
+ /**
1105
+ * Structural validation against the JsonSchema subset the layer uses.
1106
+ *
1107
+ * Constrained decoding should prevent invalid structures at generation time,
1108
+ * but a provider without native support can still return free text. This
1109
+ * validator is the deterministic gate that decides whether a response satisfies
1110
+ * its output contract — and, when it does not, whether a single repair is
1111
+ * warranted. NOTE: structural validity is NOT semantic validity; risky tasks
1112
+ * still need a semantic check on top.
1113
+ */
1114
+ interface ValidationError {
1115
+ path: string;
1116
+ message: string;
1117
+ }
1118
+ interface ValidationResult {
1119
+ valid: boolean;
1120
+ errors: ValidationError[];
1121
+ }
1122
+ declare function validateAgainstSchema(value: unknown, schema: JsonSchema, path?: string): ValidationResult;
1123
+
1124
+ /**
1125
+ * Schema registry.
1126
+ *
1127
+ * Output contracts are referenced by id (e.g. "research-answer:v3") so they do
1128
+ * not have to be re-transmitted in every prompt once the server knows them.
1129
+ * The registry also enforces "minimal contract" hygiene — arrays should be
1130
+ * bounded, strings length-capped, `additionalProperties` closed — which is how
1131
+ * we keep OUTPUT tokens down, not just input tokens.
1132
+ */
1133
+ declare class SchemaRegistry {
1134
+ private readonly contracts;
1135
+ register(contract: OutputContract): this;
1136
+ resolve(contractId: string): OutputContract | undefined;
1137
+ has(contractId: string): boolean;
1138
+ /**
1139
+ * Lint a schema for output-token hygiene. These are warnings, not errors: an
1140
+ * unbounded array or uncapped string is legal JSON Schema but tends to blow up
1141
+ * generation cost, so the layer surfaces them.
1142
+ */
1143
+ static lint(schema: JsonSchema, path?: string): string[];
1144
+ }
1145
+
1146
+ /**
1147
+ * Constrained-decoding backends.
1148
+ *
1149
+ * The point of these libraries is NOT to compress input — it is to make an
1150
+ * invalid structure impossible to emit, which eliminates the "your JSON is
1151
+ * broken, try again" repair round-trip and lets generation stop exactly when
1152
+ * the object is complete. BV-SALA treats them as interchangeable backends
1153
+ * selected by preference order:
1154
+ *
1155
+ * provider-native -> xgrammar -> llguidance -> outlines
1156
+ *
1157
+ * They must never all run at once. This module resolves which backend applies
1158
+ * for a given (provider, schema) and exposes a stable descriptor; the actual
1159
+ * runtime binding lives in the provider adapters.
1160
+ */
1161
+ type ConstraintBackendId = "provider-native" | "xgrammar" | "llguidance" | "outlines" | "none";
1162
+ interface ConstraintBackend {
1163
+ readonly id: ConstraintBackendId;
1164
+ /** Can this backend enforce the given schema for the given model? */
1165
+ supports(model: string, schema: JsonSchema): boolean;
1166
+ }
1167
+ interface ConstraintPlan {
1168
+ backend: ConstraintBackendId;
1169
+ reason: string;
1170
+ }
1171
+ /**
1172
+ * Resolve a constraint backend from an ordered preference list. Each candidate
1173
+ * declares whether it supports the (model, schema) pair; the first supporting
1174
+ * backend wins. A conformance suite (see benchmarks/) must confirm that a
1175
+ * backend which *claims* support actually enforces every constraint — declared
1176
+ * support and empirical coverage are not the same thing.
1177
+ */
1178
+ declare function resolveConstraintBackend(model: string, schema: JsonSchema, backends: ConstraintBackend[]): ConstraintPlan;
1179
+ /** A permissive backend used when the provider validates structured output natively. */
1180
+ declare function providerNativeBackend(isSupported?: (model: string, schema: JsonSchema) => boolean): ConstraintBackend;
1181
+
1182
+ /**
1183
+ * Context compression (LLMLingua-style), as an OPTIONAL sidecar.
1184
+ *
1185
+ * Two rules make this safe and worthwhile:
1186
+ *
1187
+ * 1. ORDER: eliminate context before compressing it. Dedupe -> rerank ->
1188
+ * select top-k -> extract relevant spans -> only THEN compress what remains.
1189
+ * Compressing 20 retrieved docs is worse than dropping 15 of them first.
1190
+ *
1191
+ * 2. PROTECTED SPANS: compression must never touch system instructions, safety
1192
+ * policies, schemas, grammars, exact code, identifiers, URLs, quantities,
1193
+ * dates, negations, legal clauses, quoted text, or structured tool results.
1194
+ * Those are marked and passed through verbatim; the sidecar returns a map so
1195
+ * we can verify every protected span survived (fidelity check).
1196
+ *
1197
+ * Activation is dynamic: compression adds latency and local compute, so it only
1198
+ * runs when the projected NET saving is positive.
1199
+ */
1200
+ interface ProtectedSpan {
1201
+ id: string;
1202
+ text: string;
1203
+ }
1204
+ interface CompressionRequest {
1205
+ text: string;
1206
+ protectedSpans?: ProtectedSpan[];
1207
+ /** Target compression ratio in (0,1]; 0.5 => aim for ~half the tokens. */
1208
+ targetRatio?: number;
1209
+ }
1210
+ interface CompressionResult {
1211
+ compressed: string;
1212
+ originalTokens: number;
1213
+ compressedTokens: number;
1214
+ protectedSpans: number;
1215
+ fidelityPassed: boolean;
1216
+ /** True when the sidecar was skipped and the original was returned. */
1217
+ skipped: boolean;
1218
+ reason: string;
1219
+ }
1220
+ interface Compressor {
1221
+ compress(req: CompressionRequest): Promise<CompressionResult>;
1222
+ }
1223
+ interface CompressionPolicyInput {
1224
+ contextTokens: number;
1225
+ /** Estimated tokens the compressor could remove. */
1226
+ projectedEliminable: number;
1227
+ /** Estimated local compute cost expressed in equivalent tokens. */
1228
+ compressorCostTokens: number;
1229
+ /** Minimum net token saving required to bother. */
1230
+ minimumNetTokens?: number;
1231
+ /** Do not compress contexts smaller than this. */
1232
+ minimumContextTokens?: number;
1233
+ }
1234
+ /**
1235
+ * Decide whether compression is net-positive:
1236
+ * benefit = eliminable - compressorCost - latencyPenalty - qualityRisk
1237
+ * Here we model the token side; latency/quality penalties are folded into
1238
+ * `compressorCostTokens` and `minimumNetTokens` by the caller.
1239
+ */
1240
+ declare function shouldCompress(input: CompressionPolicyInput): {
1241
+ compress: boolean;
1242
+ reason: string;
1243
+ };
1244
+ /**
1245
+ * Protected-span aware wrapper. Extracts protected spans (replacing them with
1246
+ * stable placeholders), runs the inner compressor on the remainder, restores
1247
+ * the spans, and verifies every one survived. On any fidelity failure it falls
1248
+ * back to the ORIGINAL text — a compressor that drops a negation or a number is
1249
+ * worse than no compressor at all.
1250
+ */
1251
+ declare function withProtectedSpans(inner: Compressor): Compressor;
1252
+ /** No-op compressor: the safe default until a real LLMLingua sidecar is wired in. */
1253
+ declare const noopCompressor: Compressor;
1254
+
1255
+ /**
1256
+ * Provider-neutral generation interface. Every provider (OpenAI-compatible,
1257
+ * Anthropic-compatible, Google-compatible, self-hosted) is adapted to this
1258
+ * shape so the pipeline never depends on a specific vendor SDK.
1259
+ */
1260
+ interface ProviderRequest {
1261
+ model: string;
1262
+ messages: ChatMessage[];
1263
+ tools?: ToolDescriptor[];
1264
+ /** Output contract to enforce via constrained decoding, when available. */
1265
+ schema?: JsonSchema;
1266
+ maxOutputTokens?: number;
1267
+ temperature?: number;
1268
+ seed?: number;
1269
+ /** Backend id selected by the constraints layer, for observability. */
1270
+ constraintBackend?: string;
1271
+ }
1272
+ interface ProviderUsage {
1273
+ inputTokens: number;
1274
+ outputTokens: number;
1275
+ /** Provider-reported prefix/KV-cached input tokens (ledger 4), if any. */
1276
+ cachedInputTokens?: number;
1277
+ }
1278
+ interface ProviderResponse {
1279
+ text: string;
1280
+ usage: ProviderUsage;
1281
+ /** Whether the provider enforced the schema natively. */
1282
+ structuredNatively?: boolean;
1283
+ /**
1284
+ * Por qué paró de generar el modelo. `"length"` significa que se quedó sin
1285
+ * presupuesto de salida, no que terminara.
1286
+ *
1287
+ * No es un dato decorativo: distingue una respuesta MALA de una respuesta
1288
+ * CORTADA, y esas dos no se arreglan igual. Un JSON truncado a media cadena no
1289
+ * valida, y reintentar con el MISMO tope vuelve a truncarlo — se paga una
1290
+ * segunda llamada para obtener el mismo fallo. Sin este campo el layer no
1291
+ * podía distinguirlo y hacía exactamente eso.
1292
+ */
1293
+ finishReason?: "stop" | "length" | "content_filter" | "tool_calls" | (string & {});
1294
+ raw?: unknown;
1295
+ }
1296
+ interface Provider {
1297
+ readonly id: string;
1298
+ generate(req: ProviderRequest): Promise<ProviderResponse>;
1299
+ }
1300
+
1301
+ /**
1302
+ * Deterministic mock provider.
1303
+ *
1304
+ * Lets the whole layer run — and be tested — with no network and no
1305
+ * credentials. When a schema is supplied it fabricates a schema-valid object,
1306
+ * demonstrating the constrained-decoding contract end to end. Token usage is
1307
+ * derived from the heuristic estimator so SavingsReport numbers are meaningful
1308
+ * in examples and tests.
1309
+ */
1310
+ interface MockProviderOptions {
1311
+ /** Fixed text to return when no schema is present. */
1312
+ answer?: string;
1313
+ /** Override the fabricated structured object. */
1314
+ data?: unknown;
1315
+ /** Simulated provider-cached input tokens (ledger 4). */
1316
+ cachedInputTokens?: number;
1317
+ }
1318
+ declare class MockProvider implements Provider {
1319
+ readonly id = "mock";
1320
+ private readonly options;
1321
+ constructor(options?: MockProviderOptions);
1322
+ generate(req: ProviderRequest): Promise<ProviderResponse>;
1323
+ }
1324
+ declare function createMockProvider(options?: MockProviderOptions): Provider;
1325
+
1326
+ /**
1327
+ * OpenAI-compatible provider adapter (chat/completions shape).
1328
+ *
1329
+ * Works with any endpoint that speaks the OpenAI Chat Completions API — OpenAI,
1330
+ * Azure, vLLM, SGLang, TGI, a local gateway, or Vercel AI Gateway with a
1331
+ * "provider/model" string. Structured output is requested via `response_format`
1332
+ * with a JSON schema when a contract is present, which is the provider-native
1333
+ * constrained-decoding path preferred by the layer.
1334
+ *
1335
+ * Network calls use global `fetch` (Node >= 18) so there is no SDK dependency.
1336
+ */
1337
+ interface OpenAICompatibleOptions {
1338
+ baseUrl: string;
1339
+ apiKey?: string;
1340
+ /** Strip the "provider/" prefix from model ids before sending. */
1341
+ stripProviderPrefix?: boolean;
1342
+ /** Extra headers (e.g. for a gateway). */
1343
+ headers?: Record<string, string>;
1344
+ /** Injectable fetch for testing. */
1345
+ fetchImpl?: typeof fetch;
1346
+ /**
1347
+ * When true (recommended for measurement), a response whose `usage` block is
1348
+ * missing or reports zero prompt/completion tokens for a call that actually
1349
+ * ran is treated as a HARD ERROR instead of being silently coerced to
1350
+ * 0 tokens. This closes the single most dishonest failure mode: an absent
1351
+ * usage payload otherwise becomes `actual cost = 0` → a fabricated 100%
1352
+ * saving. Benchmarks that make real cost claims must set this.
1353
+ */
1354
+ requireUsage?: boolean;
1355
+ }
1356
+ /**
1357
+ * ¿Cumple este esquema las reglas del modo `strict` de OpenAI?
1358
+ *
1359
+ * Son dos, y aplican a TODO objeto anidado:
1360
+ * 1. `additionalProperties: false`.
1361
+ * 2. `required` tiene que listar TODAS las claves de `properties` — no solo
1362
+ * las obligatorias. En modo strict no existe el campo opcional.
1363
+ *
1364
+ * Un esquema perfectamente válido como
1365
+ * { properties: { answer, refs }, required: ["answer"] }
1366
+ * incumple la segunda y el proveedor responde 400. No es un fallo del cliente:
1367
+ * es una restricción de OpenAI que JSON Schema no tiene.
1368
+ */
1369
+ declare function isStrictCompatible(schema: unknown): boolean;
1370
+ declare class OpenAICompatibleProvider implements Provider {
1371
+ readonly id: string;
1372
+ private readonly opts;
1373
+ private readonly fetchImpl;
1374
+ constructor(opts: OpenAICompatibleOptions);
1375
+ generate(req: ProviderRequest): Promise<ProviderResponse>;
1376
+ }
1377
+
1378
+ /**
1379
+ * Trace collector: an ordered log of pipeline decisions with elapsed timings.
1380
+ * Every stage records why it did what it did, so a SavingsReport can be audited
1381
+ * and so ablations can attribute savings to a specific module.
1382
+ */
1383
+ declare class TraceCollector {
1384
+ private readonly events;
1385
+ private readonly start;
1386
+ private readonly now;
1387
+ constructor(now?: () => number);
1388
+ record(stage: string, outcome: string, detail?: Record<string, unknown>): void;
1389
+ elapsed(): number;
1390
+ all(): TraceEvent[];
1391
+ }
1392
+
1393
+ /**
1394
+ * Thrown when the pipeline is asked to run without a valid, active BiVelio
1395
+ * license. This is the fail-closed contract: a paid SDK must refuse to operate
1396
+ * unlicensed, rather than silently serving requests for free.
1397
+ */
1398
+ declare class LicenseError extends Error {
1399
+ /** Resolved license status, when known: expired | invalid | revoked | none. */
1400
+ readonly status?: string;
1401
+ constructor(message: string, status?: string);
1402
+ }
1403
+
1404
+ /**
1405
+ * Offline license verification for the paid SDK.
1406
+ *
1407
+ * A license is a compact signed token `base64url(header).base64url(payload).
1408
+ * base64url(sig)` (the exact format the licensing service mints in
1409
+ * `@bivelio/license-client`'s `signLicense`). We verify the Ed25519 signature
1410
+ * against BiVelio's public key, decode the payload, and apply the same grace
1411
+ * policy the license-client uses. Verification is **browser-safe**: it uses
1412
+ * `@noble/curves` (pure JS) + Web primitives (`atob`, `TextEncoder`) rather than
1413
+ * `node:crypto`, so the shared `src/index.ts` still compiles into the browser
1414
+ * demo bundle. The private signing key never leaves the control plane; only the
1415
+ * public key is embedded here, so a token cannot be forged or tampered with.
1416
+ */
1417
+ type LicenseStatus = "active" | "grace" | "expired" | "invalid" | "revoked";
1418
+ /** Decoded license token body (mirror of `@bivelio/protocol`'s LicensePayload,
1419
+ * declared locally so the SDK stays free of a control-plane dependency). */
1420
+ interface LicenseTokenPayload {
1421
+ jti: string;
1422
+ org_id: string;
1423
+ plan: string;
1424
+ seats: number;
1425
+ features: string[];
1426
+ issued_at: number;
1427
+ expires_at: number;
1428
+ grace_days: number;
1429
+ iss?: string;
1430
+ }
1431
+ interface ResolvedLicense {
1432
+ status: LicenseStatus;
1433
+ payload?: LicenseTokenPayload;
1434
+ reason?: string;
1435
+ }
1436
+ declare const EMBEDDED_PUBLIC_KEY: string;
1437
+ /** Result of verifying a compact `head.body.sig` token's signature. */
1438
+ interface CompactVerification {
1439
+ /** True only if the signature is valid for the given public key. */
1440
+ ok: boolean;
1441
+ /** Decoded header (present when `ok`), e.g. `{ alg, typ, v }`. */
1442
+ header?: Record<string, unknown>;
1443
+ /** Raw payload bytes (present when `ok`); the caller decodes/parses them. */
1444
+ payloadBytes?: Uint8Array;
1445
+ reason?: string;
1446
+ }
1447
+ /**
1448
+ * A pinned set of Ed25519 public keys addressed by key-id (`kid`), enabling signing
1449
+ * key ROTATION without breaking already-deployed SDKs: new keys are added as extra
1450
+ * map entries, and tokens carry the `kid` they were signed with. `defaultKid` is the
1451
+ * key used for legacy tokens that carry NO `kid` (so today's single-key tokens keep
1452
+ * verifying unchanged). Verification FAILS CLOSED on a `kid` not in the map.
1453
+ */
1454
+ interface LicenseKeyset {
1455
+ /** kid → SPKI PEM. */
1456
+ keys: Record<string, string>;
1457
+ /** kid used for tokens without a `kid` header (the current/legacy key). */
1458
+ defaultKid: string;
1459
+ }
1460
+ /** Narrow a verification key to a {@link LicenseKeyset} (vs a bare SPKI PEM string). */
1461
+ declare function isKeyset(k: string | LicenseKeyset): k is LicenseKeyset;
1462
+ /**
1463
+ * Verify the Ed25519 signature of any BV-SALA compact token
1464
+ * (`base64url(header).base64url(body).base64url(sig)`, sig over `${head}.${body}`)
1465
+ * and return the decoded header + raw payload bytes. This is the single shared
1466
+ * crypto primitive: license tokens AND signed revocation lists both go through it,
1467
+ * so the browser (`@noble/curves`) and node (`node:crypto`) sides agree byte-for-byte
1468
+ * and there is no bespoke per-message verification path to keep in sync. It does NOT
1469
+ * interpret the payload or the header `typ` — callers do that.
1470
+ */
1471
+ declare function verifyCompactSignature(token: string, publicKey: string | LicenseKeyset): CompactVerification;
1472
+ /** Verify a token's signature and decode its payload. Does not check expiry. */
1473
+ declare function verifyLicenseToken(token: string, publicKey: string | LicenseKeyset): {
1474
+ signatureValid: boolean;
1475
+ payload?: LicenseTokenPayload;
1476
+ reason?: string;
1477
+ };
1478
+ /**
1479
+ * Apply the grace + revocation policy to an already-decoded payload → a status.
1480
+ * Pure and clock-injected, so a LIVE gate can call it on every request to pick up
1481
+ * both revocation (jti in the set) AND expiry transitions without re-verifying the
1482
+ * signature. Revocation takes precedence over the time-based status.
1483
+ * jti on the revocation set → revoked
1484
+ * now <= expires_at → active
1485
+ * expires_at < now <= +grace_days → grace (still entitled)
1486
+ * now > expires_at + grace_days → expired
1487
+ */
1488
+ declare function deriveStatus(payload: {
1489
+ jti: string;
1490
+ expires_at: number;
1491
+ grace_days: number;
1492
+ }, nowSeconds: number, revoked?: ReadonlySet<string>): LicenseStatus;
1493
+ /**
1494
+ * Verify + apply the grace policy → a resolved status. Mirrors
1495
+ * `@bivelio/license-client`'s `LicenseClient.resolve`. Bad signature / malformed
1496
+ * → invalid; otherwise the status comes from {@link deriveStatus}.
1497
+ */
1498
+ declare function resolveLicense(token: string, publicKey: string | LicenseKeyset, opts?: {
1499
+ nowSeconds?: number;
1500
+ revoked?: ReadonlySet<string>;
1501
+ }): ResolvedLicense;
1502
+
1503
+ /** Decoded revocation-list body (mirror of @bivelio/protocol's RevocationListPayload). */
1504
+ interface RevocationListBody {
1505
+ v: number;
1506
+ asOf: number;
1507
+ seq: number;
1508
+ revoked: string[];
1509
+ iss?: string;
1510
+ }
1511
+ interface RevocationListVerification {
1512
+ /** True only if signature + typ + monotonic-seq + skew all pass. */
1513
+ ok: boolean;
1514
+ /** List revision (monotonic). -1 when not ok. */
1515
+ seq: number;
1516
+ /** Unix seconds the list was minted. 0 when not ok. */
1517
+ asOf: number;
1518
+ /** Revoked jti in this list (empty when not ok). */
1519
+ revoked: Set<string>;
1520
+ reason?: string;
1521
+ }
1522
+ /**
1523
+ * Verify a signed revocation-list token offline and enforce the client-side
1524
+ * policy: (1) valid Ed25519 signature, (2) header `typ` is a revocation list
1525
+ * (a license token can't be replayed here), (3) `seq >= minSeq` — reject a
1526
+ * stale/rolled-back list, (4) `asOf <= now + maxSkew` — reject a future-dated list.
1527
+ * ANY failure returns `ok:false` with an empty set; the poller then keeps its
1528
+ * sticky last-known-good (fail-open), never dropping known revocations.
1529
+ */
1530
+ declare function verifyRevocationList(token: string, publicKey: string | LicenseKeyset, opts?: {
1531
+ minSeq?: number;
1532
+ nowSeconds?: number;
1533
+ maxSkewSeconds?: number;
1534
+ }): RevocationListVerification;
1535
+ /**
1536
+ * The SDK's live, sticky, monotonic view of the revocation set. It only ever
1537
+ * GROWS (a jti, once seen revoked, stays revoked for the process lifetime) and
1538
+ * only advances on a higher-or-equal `seq` — so a stale, rolled-back, or empty
1539
+ * list can never un-revoke a license. The live gate reads {@link snapshot} on
1540
+ * every request; the poller feeds it via {@link apply}.
1541
+ */
1542
+ declare class RevocationSnapshot {
1543
+ private _seq;
1544
+ private _asOf;
1545
+ private _lastContactAt;
1546
+ private readonly set;
1547
+ get seq(): number;
1548
+ get asOf(): number;
1549
+ /** Unix seconds of the last CONFIRMED authorized heartbeat (a 304 or a verified
1550
+ * list). Feeds the live gate's online staleness check; 0 until the first one. */
1551
+ get lastContactAt(): number;
1552
+ /** Record a confirmed authorized heartbeat from the control plane (monotonic). */
1553
+ markContact(nowSeconds: number): void;
1554
+ has(jti: string): boolean;
1555
+ /** Live view of the revoked set (the same Set instance — read-only to callers). */
1556
+ snapshot(): ReadonlySet<string>;
1557
+ /**
1558
+ * Merge an already-verified list. Accepts only `seq >= current seq`; UNIONS the
1559
+ * jti (never removes); advances seq/asOf monotonically. Returns whether the
1560
+ * revoked set grew or the revision advanced.
1561
+ */
1562
+ apply(list: {
1563
+ seq: number;
1564
+ asOf: number;
1565
+ revoked: Iterable<string>;
1566
+ }): boolean;
1567
+ }
1568
+
1569
+ /** A resolved gate that also exposes overall status + a refresh horizon. */
1570
+ interface ResolvedGate extends LicenseGate {
1571
+ isActive(): boolean;
1572
+ status(): LicenseStatus;
1573
+ /** Unix seconds after which the online status should be re-checked (poller anchor). */
1574
+ refreshAfter?(): number | undefined;
1575
+ }
1576
+ /**
1577
+ * Build a LIVE license gate from a signed key. The token is verified + decoded
1578
+ * ONCE (no per-request Ed25519 verify — hot-path safe); each `isActive()` /
1579
+ * `status()` / `isEntitled()` then re-derives the status from the cached payload +
1580
+ * the LIVE clock + the (mutable, sticky) {@link RevocationSnapshot}. This is what
1581
+ * lets a revocation — or an expiry crossed mid-process — take effect without a
1582
+ * restart, while the customer request path never touches the network (it reads an
1583
+ * in-memory Set).
1584
+ *
1585
+ * The offline path passes a fresh, never-updated snapshot (live clock, zero
1586
+ * network); the online path passes a snapshot a {@link RevocationPoller} feeds.
1587
+ */
1588
+ declare function liveGateFromKey(licenseKey: string, publicKey: string | LicenseKeyset, opts: {
1589
+ revocation: RevocationSnapshot;
1590
+ now?: () => number;
1591
+ /** Online staleness ceiling, seconds. If the gate goes this long without a
1592
+ * confirmed authorized heartbeat, it stops granting (closes the block-egress
1593
+ * fail-open). Undefined = check disabled (the offline path never sets it). */
1594
+ maxStalenessSeconds?: number;
1595
+ /** Freshness anchor (unix seconds); defaults to construction time (boot). */
1596
+ startedAt?: number;
1597
+ }): ResolvedGate;
1598
+
1599
+ /**
1600
+ * Build the SDK's structural {@link import("../contracts/types.js").LicenseGate}
1601
+ * from a signed license key — the object `createSavingsLayer({ licenseKey })`
1602
+ * injects on the OFFLINE path (no `licenseServerUrl`).
1603
+ *
1604
+ * It is a LIVE gate over an EMPTY, never-updated revocation snapshot: zero network,
1605
+ * but the clock is LIVE, so a license that crosses its expiry (past grace) stops
1606
+ * working mid-process instead of only after a restart — closing the frozen-clock
1607
+ * hole the old one-shot resolution left open. `isActive()` is true while active or
1608
+ * in grace; `isEntitled` additionally requires the feature in the token's grant.
1609
+ * Online revocation is added by pairing {@link liveGateFromKey} with a
1610
+ * poller-fed snapshot (see src/pipeline/create.ts + poller.ts).
1611
+ */
1612
+ declare function gateFromKey(licenseKey: string, publicKey: string | LicenseKeyset, now?: () => number): ResolvedGate;
1613
+
1614
+ type RevocationPollEvent = {
1615
+ type: "applied";
1616
+ seq: number;
1617
+ size: number;
1618
+ } | {
1619
+ type: "unchanged";
1620
+ seq: number;
1621
+ } | {
1622
+ type: "rejected";
1623
+ reason: string;
1624
+ } | {
1625
+ type: "error";
1626
+ error: unknown;
1627
+ } | {
1628
+ type: "no-fetch";
1629
+ };
1630
+ interface RevocationPollerOptions {
1631
+ /** Absolute URL of GET /licenses/revocations. */
1632
+ url: string;
1633
+ /** SPKI PEM (or a kid-addressed {@link LicenseKeyset}) used to verify the signed
1634
+ * list — the same key/keyset that verifies license tokens. */
1635
+ publicKeyPem: string | LicenseKeyset;
1636
+ /** The sticky snapshot to feed. */
1637
+ snapshot: RevocationSnapshot;
1638
+ /** Anchor cadence to the license refresh horizon (unix seconds). */
1639
+ getRefreshAfter?: () => number | undefined;
1640
+ /** Injectable clock (unix seconds). */
1641
+ nowSeconds?: () => number;
1642
+ /** Injectable fetch (defaults to the global). */
1643
+ fetchImpl?: typeof fetch;
1644
+ pollIntervalSeconds?: number;
1645
+ floorSeconds?: number;
1646
+ maxIntervalSeconds?: number;
1647
+ jitterRatio?: number;
1648
+ maxSkewSeconds?: number;
1649
+ requestTimeoutMs?: number;
1650
+ /** Injectable RNG in [0,1) for jitter. Default Math.random. */
1651
+ random?: () => number;
1652
+ /** Optional observer (telemetry/tests). */
1653
+ onEvent?: (e: RevocationPollEvent) => void;
1654
+ }
1655
+ /**
1656
+ * PURE, unit-testable next-delay policy (seconds). Steady state anchors to the
1657
+ * license refresh horizon, clamped to [floor, ceiling] and jittered. After a
1658
+ * failure it backs off exponentially (a revocation can't land during an outage
1659
+ * anyway), capped, still jittered, never below the floor.
1660
+ */
1661
+ declare function computeNextDelay(opts: {
1662
+ refreshAfter?: number;
1663
+ nowSeconds: number;
1664
+ consecutiveFailures: number;
1665
+ floorSeconds: number;
1666
+ pollIntervalSeconds: number;
1667
+ maxIntervalSeconds: number;
1668
+ jitterRatio: number;
1669
+ random?: () => number;
1670
+ }): number;
1671
+ declare class RevocationPoller {
1672
+ private readonly opts;
1673
+ private timer;
1674
+ private controller;
1675
+ private consecutiveFailures;
1676
+ private stopped;
1677
+ private started;
1678
+ private warnedNoFetch;
1679
+ /** ETag of the last 200; sent as If-None-Match so an unchanged list 304s (cheap no-op). */
1680
+ private lastEtag;
1681
+ /** 304 seguidos; al llegar al tope se fuerza una descarga firmada. */
1682
+ private consecutive304;
1683
+ constructor(opts: RevocationPollerOptions);
1684
+ private now;
1685
+ private resolveFetch;
1686
+ /**
1687
+ * One poll cycle: fetch → verify → apply. NEVER throws (fail-open). A reachable
1688
+ * CP resets the failure backoff even if the returned list is rejected — we keep
1689
+ * last-known-good. Only a transport/abort/parse error counts as a failure.
1690
+ */
1691
+ pollOnce(): Promise<void>;
1692
+ /** Fire one non-blocking warm poll, then self-schedule. Idempotent. */
1693
+ start(): void;
1694
+ private schedule;
1695
+ /** Stop polling and abort any in-flight request. Idempotent. */
1696
+ stop(): void;
1697
+ }
1698
+
1699
+ interface UsageEmitterOptions {
1700
+ /** Full ingest URL, e.g. `https://savings.bivelio.com/api/metering/usage`. */
1701
+ url: string;
1702
+ /** Service-account key (`bvk_…`) sent as a Bearer token; the server derives the org from it. */
1703
+ serviceAccountKey: string;
1704
+ /** Optional org hint; the server always derives the authoritative org from the key. */
1705
+ orgId?: string;
1706
+ /** Flush cadence in ms (default 15000). */
1707
+ flushIntervalMs?: number;
1708
+ /** Records per POST (default 50). */
1709
+ maxBatch?: number;
1710
+ /** Hard in-memory cap; oldest records are dropped past it (default 1000). */
1711
+ maxBuffer?: number;
1712
+ /** Injectable fetch (defaults to the global). */
1713
+ fetchImpl?: typeof fetch;
1714
+ /** Injectable clock. */
1715
+ now?: () => number;
1716
+ /** Whether this process is a CI/ephemeral runner (excluded from billable seats). */
1717
+ ci?: boolean;
1718
+ }
1719
+ /**
1720
+ * Buffers counts-only usage and flushes it to the control plane. DETECTIVE
1721
+ * metering: strictly best-effort and NON-BLOCKING. `record()` is O(1) and
1722
+ * `flush()` never throws, so a slow, unreachable, or erroring control plane can
1723
+ * neither block nor crash the customer's request path. The buffer is hard-capped
1724
+ * so a sustained outage can never grow memory without bound.
1725
+ */
1726
+ declare class UsageEmitter {
1727
+ private readonly options;
1728
+ private readonly now;
1729
+ private readonly flushIntervalMs;
1730
+ private readonly maxBatch;
1731
+ private readonly maxBuffer;
1732
+ private buffer;
1733
+ private seatId;
1734
+ private ci;
1735
+ private timer;
1736
+ private flushing;
1737
+ constructor(options: UsageEmitterOptions);
1738
+ /** Set the resolved seat identity (derivation is async; records emitted before
1739
+ * it resolves are stamped at flush time, so none are lost). */
1740
+ setSeat(seatId: string | undefined, ci: boolean): void;
1741
+ /** Record one request's usage. Never blocks; O(1). */
1742
+ record(report: SavingsReport): void;
1743
+ start(): void;
1744
+ /** Stop the timer and attempt a final flush. Safe to call more than once. */
1745
+ stop(): void;
1746
+ /**
1747
+ * Flush one batch. Never throws. A transient server error (5xx) or network
1748
+ * failure re-queues the batch (capped); a 4xx (bad request / bad key) drops it,
1749
+ * since retrying a rejected payload would loop forever.
1750
+ */
1751
+ flush(): Promise<void>;
1752
+ private requeue;
1753
+ private toWire;
1754
+ }
1755
+
1756
+ /**
1757
+ * DETECTIVE seat identity for usage metering. A "seat" is a distinct MACHINE /
1758
+ * service identity — NEVER a human. `deriveSeatId` produces a stable, opaque,
1759
+ * per-account id by HMAC-ing a machine fingerprint with the service-account key:
1760
+ * - stable across restarts on the same machine (same billable seat),
1761
+ * - never reveals the fingerprint (HMAC keyed by the secret SA key),
1762
+ * - different accounts on the same machine get different ids.
1763
+ *
1764
+ * Universal + dependency-free: uses only WebCrypto (`globalThis.crypto.subtle`)
1765
+ * and ambient `process` env, both guarded — so this module carries NO Node builtin
1766
+ * import and is safe in the browser bundle (where it simply yields no seat id).
1767
+ */
1768
+ /** True when running in a CI / ephemeral environment. */
1769
+ declare function detectCi(): boolean;
1770
+ /**
1771
+ * Derive the machine seat id for a service-account key. Resolves to `undefined`
1772
+ * when no machine fingerprint is available (e.g. a browser) — the caller then
1773
+ * reports no seatId and the server counts the request under an "unknown" seat.
1774
+ * The id (`m-<32 hex>`) matches the control plane's identity charset.
1775
+ */
1776
+ declare function deriveSeatId(serviceAccountKey: string): Promise<string | undefined>;
1777
+
1778
+ /**
1779
+ * Everything the pipeline depends on. All fields except `provider` and
1780
+ * `tokenizer` are optional; sensible in-memory defaults are wired by
1781
+ * {@link createSavingsLayer} so the layer works out of the box and every
1782
+ * component is independently swappable.
1783
+ */
1784
+ interface SavingsLayerConfig {
1785
+ /**
1786
+ * Contratos a los que ya se les incumplió una restricción de valor.
1787
+ *
1788
+ * La capa lo aprende sola, pero lo aprendido muere con la instancia. En
1789
+ * serverless —una instancia por petición— eso significa no aprender NUNCA:
1790
+ * cada llamada estrena y paga su reparación. Pasar aquí un conjunto
1791
+ * compartido (o respaldado por Redis, con la API de Set) hace que se aprenda
1792
+ * una vez para todo el despliegue.
1793
+ *
1794
+ * Solo guarda identificadores de contrato, nunca contenido de peticiones.
1795
+ */
1796
+ constraintMemory?: Set<string>;
1797
+ provider: Provider;
1798
+ tokenizer: TokenizerRegistry;
1799
+ exactCache?: ExactCache;
1800
+ semanticCache?: InMemorySemanticCache;
1801
+ singleflight?: Singleflight;
1802
+ resolvers?: ResolverRegistry;
1803
+ router?: ExecutionRouter;
1804
+ toolSelector?: ToolSelector;
1805
+ formatSelector?: FormatSelector;
1806
+ schemaRegistry?: SchemaRegistry;
1807
+ constraintBackends?: ConstraintBackend[];
1808
+ compressor?: Compressor;
1809
+ retriever?: Retriever;
1810
+ reranker?: Reranker;
1811
+ /**
1812
+ * Signed BiVelio license token (from your paid subscription). This is the
1813
+ * primary way to license the SDK: `createSavingsLayer` verifies it offline
1814
+ * against the embedded public key and builds a {@link licenseGate} from it.
1815
+ * Without a valid, active license (and without {@link devUnlicensed}), the
1816
+ * pipeline refuses to run — get a key at https://savings.bivelio.com.
1817
+ */
1818
+ licenseKey?: string;
1819
+ /**
1820
+ * Override the public key used to verify {@link licenseKey}. An SPKI PEM string,
1821
+ * or a kid-addressed {@link LicenseKeyset} (`{ keys: {kid: pem}, defaultKid }`)
1822
+ * to support signing-key rotation — legacy no-kid tokens use `defaultKid`, and
1823
+ * an unknown kid fails closed. For self-hosting/self-testing; defaults to the
1824
+ * build-embedded BiVelio key.
1825
+ */
1826
+ licensePublicKey?: string | LicenseKeyset;
1827
+ /**
1828
+ * Pre-built license gate. Usually you pass {@link licenseKey} instead and let
1829
+ * the SDK construct this. Advanced/self-managed callers can inject their own
1830
+ * gate (e.g. `@bivelio/license-client`'s `createLicenseGate()`). When both are
1831
+ * given, `licenseGate` wins.
1832
+ */
1833
+ licenseGate?: LicenseGate;
1834
+ /**
1835
+ * OPT-IN online revocation call-home. When set (alongside {@link licenseKey} and
1836
+ * with no explicit {@link licenseGate}), the SDK polls this base's
1837
+ * `GET /licenses/revocations` for a signed revocation list and enforces it via a
1838
+ * LIVE gate — a license cancelled mid-term stops working without a restart.
1839
+ *
1840
+ * NEVER-BRICK: the customer request path never blocks on the fetch; the warm poll
1841
+ * is fired but not awaited, and any failed/unreachable poll is a no-op that keeps
1842
+ * the last-known-good snapshot. Leaving this unset keeps the fully-offline,
1843
+ * zero-network default path. Pass the base URL of the licensing service, e.g.
1844
+ * `https://savings.bivelio.com`.
1845
+ */
1846
+ licenseServerUrl?: string;
1847
+ /**
1848
+ * Steady-state ceiling between revocation polls, in seconds (default 900 = 15 min).
1849
+ * The effective cadence tightens near license expiry and has a hard floor.
1850
+ */
1851
+ licensePollIntervalSeconds?: number;
1852
+ /** Injectable `fetch` for the revocation poller (defaults to the global). Testing/proxy. */
1853
+ licenseFetch?: typeof fetch;
1854
+ /** Advanced tuning of the revocation poller cadence + skew tolerance. */
1855
+ licenseRevocation?: {
1856
+ /** Hard floor between polls, seconds (default 300). */
1857
+ floorSeconds?: number;
1858
+ /** Backoff ceiling during a control-plane outage, seconds (default 3600). */
1859
+ maxIntervalSeconds?: number;
1860
+ /** Jitter fraction ± (default 0.1). */
1861
+ jitterRatio?: number;
1862
+ /** Reject a signed list whose `asOf` is more than this far in the future, seconds (default 300). */
1863
+ maxSkewSeconds?: number;
1864
+ /**
1865
+ * ONLINE staleness ceiling, seconds (default ~6h). If the SDK goes this long
1866
+ * without a CONFIRMED authorized heartbeat (a 304 or a verified list), the LIVE
1867
+ * gate stops granting — closing the fail-open window where blocking egress keeps
1868
+ * a revoked license alive until natural expiry. Must exceed the poll cadence
1869
+ * with margin. Applies ONLY on the online path ({@link licenseServerUrl} set);
1870
+ * it is NOT a zero-network opt-out — use the offline path for that.
1871
+ */
1872
+ maxStalenessSeconds?: number;
1873
+ };
1874
+ /**
1875
+ * Service-account key (`bvk_…`) that authenticates DETECTIVE usage metering to
1876
+ * the control plane. When set (alongside {@link licenseServerUrl} or an explicit
1877
+ * {@link meteringUrl}), the SDK reports COUNTS-ONLY usage after each request —
1878
+ * request id, model, cache outcome, and token/cost counts, tagged with an
1879
+ * auto-derived machine seat id — NEVER prompts or completions. Best-effort and
1880
+ * non-blocking: a slow or unreachable control plane never blocks or breaks
1881
+ * `generate()`. Get the key from your dashboard.
1882
+ */
1883
+ serviceAccountKey?: string;
1884
+ /**
1885
+ * Usage-ingest URL. Defaults to `${licenseServerUrl}/api/metering/usage`. Set
1886
+ * explicitly to point metering at a different host than the revocation poller.
1887
+ */
1888
+ meteringUrl?: string;
1889
+ /** Injectable `fetch` for metering (defaults to {@link licenseFetch}, then global). */
1890
+ meteringFetch?: typeof fetch;
1891
+ /** DETECTIVE metering tuning (all optional). */
1892
+ metering?: {
1893
+ /** Off switch even when {@link serviceAccountKey} is set (default: on). */
1894
+ enabled?: boolean;
1895
+ /** Flush cadence in ms (default 15000). */
1896
+ flushIntervalMs?: number;
1897
+ /** Records per POST (default 50). */
1898
+ maxBatch?: number;
1899
+ /** Hard in-memory cap; oldest records dropped past it (default 1000). */
1900
+ maxBuffer?: number;
1901
+ /** Override the auto-derived machine seat id. */
1902
+ seatId?: string;
1903
+ /** Override CI auto-detection (CI/ephemeral seats are excluded from billable seats). */
1904
+ ci?: boolean;
1905
+ /** Optional org hint; the server derives the authoritative org from the key. */
1906
+ orgId?: string;
1907
+ };
1908
+ /**
1909
+ * Development-only escape hatch to run ungated, honored ONLY when building from
1910
+ * source (local dev, CI, benchmarks) — also via the `BVSALA_DEV_OPEN=1`
1911
+ * environment variable.
1912
+ *
1913
+ * IMPORTANT: this flag is **disabled in the published npm package**. The
1914
+ * distributed build hard-codes the licensing check on, so setting
1915
+ * `devUnlicensed` (or `BVSALA_DEV_OPEN`) there has NO effect — the layer still
1916
+ * requires a valid `licenseKey`. It is not a way to run the product for free.
1917
+ */
1918
+ devUnlicensed?: boolean;
1919
+ /** Per-model cost models; falls back to `defaultCostModel`. */
1920
+ costModels?: Record<string, CostModel>;
1921
+ defaultCostModel?: CostModel;
1922
+ /** TTL for exact-cache writes, in ms. */
1923
+ exactCacheTtlMs?: number;
1924
+ /**
1925
+ * Reliability contract. When true (the default), ANY internal optimizer error
1926
+ * (a resolver, cache, format selector, compressor, or accounting bug) is
1927
+ * caught and the request falls open to a plain provider call — BV-SALA must
1928
+ * never add a crash point to the customer's request path. Set to `false` only
1929
+ * in tests/benchmarks that want internal errors to surface.
1930
+ */
1931
+ failOpen?: boolean;
1932
+ /** Injectable clock for deterministic tests. */
1933
+ now?: () => number;
1934
+ /** Injectable id generator for deterministic tests. */
1935
+ generateId?: () => string;
1936
+ }
1937
+ declare const DEFAULT_COST_MODEL: CostModel;
1938
+
1939
+ /** Fully-resolved, non-optional dependency set the pipeline runs against. */
1940
+ interface ResolvedDeps {
1941
+ exactCache: ExactCache;
1942
+ semanticCache: InMemorySemanticCache | undefined;
1943
+ singleflight: Singleflight;
1944
+ resolvers: ResolverRegistry;
1945
+ router: ExecutionRouter;
1946
+ toolSelector: ToolSelector;
1947
+ formatSelector: FormatSelector;
1948
+ schemaRegistry: SchemaRegistry;
1949
+ constraintBackends: ConstraintBackend[];
1950
+ }
1951
+ /**
1952
+ * Wire defaults for anything the caller did not supply. Every default is an
1953
+ * in-memory, dependency-free implementation, so `createSavingsLayer({ provider,
1954
+ * tokenizer })` is a working layer, and each piece can be replaced à la carte.
1955
+ */
1956
+ declare function resolveDependencies(config: SavingsLayerConfig): ResolvedDeps;
1957
+
1958
+ /** The public handle returned by {@link createSavingsLayer}. */
1959
+ interface SavingsLayer {
1960
+ generate<T = unknown>(request: SavingsRequest): Promise<SavingsResult<T>>;
1961
+ /**
1962
+ * Stop the background revocation poller (when the online call-home was enabled
1963
+ * via `licenseServerUrl`) and abort any in-flight request. Always safe to call —
1964
+ * a no-op when there is no poller. Call it when shutting down a long-running
1965
+ * process so the interval timer and any pending fetch are released.
1966
+ */
1967
+ close(): void;
1968
+ }
1969
+ /**
1970
+ * Create a savings layer. The only hard requirement is a `provider`; a default
1971
+ * heuristic tokenizer and in-memory implementations of every other component
1972
+ * are wired automatically. Replace any of them for production (Redis exact
1973
+ * cache, pgvector/Qdrant semantic cache, a real tokenizer, an LLMLingua sidecar,
1974
+ * XGrammar/llguidance backends, etc.).
1975
+ *
1976
+ * @example
1977
+ * const layer = createSavingsLayer({ provider: createMockProvider() });
1978
+ * const { data, report } = await layer.generate({
1979
+ * model: "openai/gpt-4o-mini",
1980
+ * messages: [{ role: "user", content: "What is 2 + 2 * 3?" }],
1981
+ * });
1982
+ */
1983
+ declare function createSavingsLayer(config: Omit<SavingsLayerConfig, "tokenizer"> & Partial<Pick<SavingsLayerConfig, "tokenizer">>): SavingsLayer;
1984
+
1985
+ /**
1986
+ * bivelio-savings-layer (BV-SALA)
1987
+ * ---------------------------------------------------------------------------
1988
+ * PROPRIETARY AND CONFIDENTIAL. Copyright (c) 2026 BiVelio Inc.
1989
+ * All Rights Reserved. Unauthorized copying, distribution, or use of this
1990
+ * software, in whole or in part, is strictly prohibited. See LICENSE.
1991
+ * ---------------------------------------------------------------------------
1992
+ *
1993
+ * A provider-neutral LLM execution optimizer. For every request it answers, in
1994
+ * order: Can I avoid this call? Can I avoid retrieval? Can I send fewer tools?
1995
+ * What context is actually needed? Should I compress it? Which format costs
1996
+ * least for this tokenizer? What is the minimal output contract? Is the saving
1997
+ * still positive after all infrastructure is counted?
1998
+ *
1999
+ * It keeps FOUR separate ledgers — avoided_calls, eliminated_tokens,
2000
+ * compressed_tokens, provider_cached_tokens — because mixing them produces
2001
+ * misleading metrics.
2002
+ */
2003
+ declare const BV_SALA_VERSION = "0.1.0";
2004
+
2005
+ export { BV_SALA_VERSION, type BivelioHandoff, type CacheOutcome, type CachedAnswer, type CanonicalRequest, type ChatMessage, type CompactVerification, type CompressionPolicyInput, type CompressionRequest, type CompressionResult, type Compressor, type ConstraintBackend, type ConstraintBackendId, type ConstraintPlan, type ConversationState, type CostBasis, type CostModel, DEFAULT_COST_MODEL, type DeterministicResolver, EMBEDDED_PUBLIC_KEY, type EmbeddingFn, type EvidenceDoc, type ExactCache, ExactTokenizer, type ExecutionClass, ExecutionRouter, type FormatChoice, FormatSelector, type FormatSelectorOptions, type HandoffFact, HeuristicEstimator, InMemoryExactCache, InMemorySemanticCache, type JsonSchema, LicenseError, type LicenseGate, type LicenseKeyset, type LicenseStatus, type LicenseTokenPayload, type LocalClassifier, MODELOS_POR_CODIFICACION, type MemoryDelta, type MessageRole, MockProvider, type MockProviderOptions, type OpenAICompatibleOptions, OpenAICompatibleProvider, type OutputContract, type Policy, type ProtectedSpan, type Provider, type ProviderRequest, type ProviderResponse, type ProviderUsage, type RequestContext, type Reranker, type ResolvedDeps, type ResolvedGate, type ResolvedLicense, ResolverRegistry, type ResolverResult, type RetrievalOptions, type RetrievalOutcome, type RetrievalPlan, type Retriever, type RevocationListBody, type RevocationListVerification, type RevocationPollEvent, RevocationPoller, type RevocationPollerOptions, RevocationSnapshot, type RouteDecision, type SavingsLayer, type SavingsLayerConfig, SavingsLedger, type SavingsReport, type SavingsRequest, type SavingsResult, type SavingsToggles, SchemaRegistry, type SemanticCacheOptions, type SemanticHit, type SemanticRecord, type SemanticVerifier, type SerializationFormat, type Serializer, Singleflight, TOON_INSTRUCTION, type TokenEstimator, TokenizerRegistry, type ToolDescriptor, type ToolRisk, type ToolSelection, ToolSelector, type ToolSelectorOptions, type ToonDecodeOptions, type ToonOptions, TraceCollector, type TraceEvent, UsageEmitter, type UsageEmitterOptions, type ValidationError, type ValidationResult, adaptiveK, arithmeticResolver, buildExactKey, buildInflightKey, canonicalize, computeNextDelay, cosine, createDefaultResolvers, createHandoff, createMockProvider, createSavingsLayer, createToonSerializer, csvSerializer, decodeToon, dedupe, defaultTokenizerRegistry, deriveSeatId, deriveStatus, detectCi, emptyState, encodeToon, evalArithmetic, evaluatePolicy, executeRetrievalPlan, gateFromKey, heuristicEstimator, isKeyset, isStrictCompatible, jsonCompactSerializer, keyTokensMatch, liveGateFromKey, mergeDelta, noopCompressor, normalizeText, projectContext, providerNativeBackend, randomId, registrarTokenizadorExacto, renderState, resolveConstraintBackend, resolveDependencies, resolveLicense, sha256, shouldCompress, stableStringify, toonSerializer, uniformColumns, validateAgainstSchema, verifyCompactSignature, verifyLicenseToken, verifyRevocationList, withProtectedSpans };