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