@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.
- package/LICENSE +69 -0
- package/README.md +152 -0
- package/dist/index.d.ts +2005 -0
- package/dist/index.js +36 -0
- package/package.json +96 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|