@bivelio/savings-layer 0.4.29 → 0.4.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -232,6 +232,20 @@ declare class SavingsLedger {
232
232
  providerCached(tokens: number): void;
233
233
  /** Record the premium for `tokens` of cache write the provider bills at `multiplier`x on its own. */
234
234
  providerCacheWritePremium(tokens: number, multiplier: number, price: CostModel): void;
235
+ /** Input tokens of calls the provider documents as not billed; already included in `actualInput`. */
236
+ unbilledInput: number;
237
+ /** Output tokens of those calls; already included in `actualOutput`. */
238
+ unbilledOutput: number;
239
+ /** Provider-cached input tokens of those calls; already included in `providerCachedInput`. */
240
+ unbilledCachedInput: number;
241
+ /** True when the answer served is a refusal the provider does not bill: no saving is credited. */
242
+ servedUnbilledRefusal: boolean;
243
+ /** Record a call the provider documents as not billed: its tokens stay counted and are priced at 0. */
244
+ unbilledCall(usage: {
245
+ inputTokens: number;
246
+ outputTokens: number;
247
+ cachedInputTokens?: number;
248
+ }): void;
235
249
  /** Set when an avoided call is valued against the provider counter of an identical call that ran. */
236
250
  reference: ReferenceCredit | null;
237
251
  /** Set when an avoided cache or coalesced hit could not be valued by reference: why it stays estimated. */
@@ -311,7 +325,7 @@ declare function precioDeLista(model: string, at?: Date | number): CostModel | u
311
325
  * on the customer's machine. `test/version-declarada.test.ts` ties this
312
326
  * constant to package.json so the two can never drift.
313
327
  */
314
- declare const BV_SALA_VERSION = "0.4.29";
328
+ declare const BV_SALA_VERSION = "0.4.31";
315
329
 
316
330
  /**
317
331
  * Environment-agnostic UUID generation.
@@ -532,7 +546,11 @@ interface OutputContract {
532
546
  /** Stable identifier, e.g. "research-answer:v3". */
533
547
  contractId: string;
534
548
  schema: JsonSchema;
535
- /** Hard ceiling on generated tokens. */
549
+ /**
550
+ * Hard ceiling on generated tokens for this contract. A free-text request (no
551
+ * schema) sets its ceiling with {@link SavingsRequest.maxOutputTokens}; when
552
+ * both are set, the smaller one is sent.
553
+ */
536
554
  maxOutputTokens?: number;
537
555
  /** Version marker included in the exact-cache key. */
538
556
  version?: string;
@@ -1089,6 +1107,19 @@ interface SavingsRequest {
1089
1107
  /** Sampling controls that participate in the exact-cache key. */
1090
1108
  temperature?: number;
1091
1109
  seed?: number;
1110
+ /**
1111
+ * Hard ceiling on generated tokens for this request, with or without an
1112
+ * output contract. It travels to every adapter: Anthropic `max_tokens`,
1113
+ * Chat Completions `max_completion_tokens` on OpenAI's own API and
1114
+ * `max_tokens` on any other OpenAI-compatible endpoint (see
1115
+ * `OpenAICompatibleOptions.maxTokensParam`). When `response.maxOutputTokens`
1116
+ * is also set, the smaller of the two is sent. Unset, nothing changes.
1117
+ *
1118
+ * It is a limit you set, not a saving: it never raises the estimated output
1119
+ * of an avoided call and is never credited as output avoided. An answer cut
1120
+ * by it (`finishReason: "length"`) is not cached. Must be a positive integer.
1121
+ */
1122
+ maxOutputTokens?: number;
1092
1123
  /** Tokenizer id to cost against; defaults to a model-derived estimate. */
1093
1124
  tokenizer?: string;
1094
1125
  /** Arbitrary structured payload to be serialized into the prompt. */
@@ -1322,11 +1353,12 @@ interface QualityTruncationCheck {
1322
1353
  /** The stop reason exactly as the provider named it; absent when it reported none. */
1323
1354
  finishReason?: string;
1324
1355
  /**
1325
- * Whether you set the limit (`response.maxOutputTokens`). When false, any
1356
+ * Whether you set the limit (`maxOutputTokens` on the request or on
1357
+ * `response`). When false, any
1326
1358
  * limit that applied was a provider or adapter default.
1327
1359
  */
1328
1360
  capSetByCaller: boolean;
1329
- /** The limit you set, in output tokens. Present only when `capSetByCaller`. */
1361
+ /** The limit that was sent (the smaller of the two when both were set). Present only when `capSetByCaller`. */
1330
1362
  outputLimit?: number;
1331
1363
  }
1332
1364
  /** How a cache hit matched this request. */
@@ -2226,8 +2258,13 @@ interface SemanticCacheOptions {
2226
2258
  *
2227
2259
  * Pass your own for true semantics; the local one only sees LEXICAL closeness.
2228
2260
  * Either way the deterministic guard below still rejects a candidate whose
2229
- * numbers, negation or entities differ, so a weaker vector can only cost you a
2230
- * cache miss — never a wrong answer.
2261
+ * numbers (in digits or spelled out in en/es/ca: "three", "twice", "half"),
2262
+ * negation parity or content words differ beyond its overlap floor. It is a
2263
+ * lexical guard, not a proof: a one-word entity swap in a long query can
2264
+ * still clear the overlap floor, and through 0.4.30 a number written in words
2265
+ * cleared it too (GSM8K: 477 of 564 such variants got their original's
2266
+ * answer). Calibrate on your own traffic before trusting it with answers
2267
+ * that differ by one word.
2231
2268
  */
2232
2269
  embed?: EmbeddingFn;
2233
2270
  /**
@@ -2544,7 +2581,20 @@ interface ToolSelection {
2544
2581
  setIsStable: boolean;
2545
2582
  }
2546
2583
  interface ToolSelectorOptions {
2547
- /** Max tools to include (schema-token budget guard). */
2584
+ /**
2585
+ * Max tools the selector keeps when it has a SIGNAL to select on: a catalogue
2586
+ * whose tools declare `intents` and/or `risk`, with the router on. Default 8.
2587
+ *
2588
+ * It never truncates a catalogue it cannot rank. A catalogue that declares no
2589
+ * `intents` and no `risk` (the usual case: MCP and OpenAI tool lists) and every
2590
+ * request with `savings: { router: false }` go to the provider WHOLE, exactly as
2591
+ * the caller sent them, and the layer credits nothing for the tool block:
2592
+ * cutting by position left the model unable to call the rest (on BFCL
2593
+ * multi-turn, gpt-4o-mini hit the reference call 8/10 times directly and 1/10
2594
+ * through the layer). When the selector does prune a typed catalogue, the
2595
+ * report's `tool-selector` trace lists what was left out and the SDK warns
2596
+ * (once per process, silenced by `silenceWarnings`).
2597
+ */
2548
2598
  maxTools?: number;
2549
2599
  /** Tokenizer-aware scorer for tie-breaking (optional). */
2550
2600
  scoreIntentMatch?: (text: string, tool: ToolDescriptor) => number;
@@ -2590,16 +2640,9 @@ declare class ToolSelector {
2590
2640
  private readonly freezer;
2591
2641
  constructor(opts?: ToolSelectorOptions);
2592
2642
  /**
2593
- * Todas las herramientas que pasó el llamante, acotadas SOLO por el
2594
- * presupuesto de esquema (`maxTools`).
2595
- *
2596
- * Se usa cuando el llamante apaga el router (`savings:{router:false}`): sin
2597
- * clasificación no hay de dónde derivar el riesgo, y filtrar sobre el
2598
- * `defaultRoute()` tiraba en silencio las herramientas de escritura. Apagar el
2599
- * router significa "yo elijo las herramientas", así que la relevancia pasa a
2600
- * ser del llamante — pero el tope de presupuesto NO se levanta: mandar 200
2601
- * esquemas al proveedor es un 400 seguro y una petición que antes funcionaba
2602
- * pasaría a fallar en duro.
2643
+ * Every tool the caller passed, as passed: none left out, none credited.
2644
+ * Used when there is no signal to select on (`savings: { router: false }`,
2645
+ * or a catalogue with no `intents` and no `risk`).
2603
2646
  */
2604
2647
  passthrough(tools: ToolDescriptor[] | undefined): ToolSelection;
2605
2648
  select(text: string, tools: ToolDescriptor[] | undefined, execution: ExecutionClass,
@@ -3000,6 +3043,10 @@ interface ProviderRequest {
3000
3043
  tools?: ToolDescriptor[];
3001
3044
  /** Output contract to enforce via constrained decoding, when available. */
3002
3045
  schema?: JsonSchema;
3046
+ /**
3047
+ * Output cap chosen by the caller: the smaller of `SavingsRequest.maxOutputTokens`
3048
+ * and `response.maxOutputTokens`. Each adapter maps it to its own field.
3049
+ */
3003
3050
  maxOutputTokens?: number;
3004
3051
  temperature?: number;
3005
3052
  seed?: number;
@@ -3189,6 +3236,19 @@ interface ProviderResponse {
3189
3236
  * replayed by reference. `text` and `finishReason` stay what the provider said.
3190
3237
  */
3191
3238
  refusal?: string;
3239
+ /**
3240
+ * True when this answer is a refusal the provider documents as NOT billed:
3241
+ * Anthropic's Messages API refusal that arrives before any output, in a
3242
+ * category its published table marks as not billed (`cyber`,
3243
+ * `general_harms`, or a `null` category). See
3244
+ * https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback
3245
+ * («How refusals are billed»).
3246
+ *
3247
+ * The layer then measures this call at cost 0 and credits no saving on the
3248
+ * request at all (measured, estimated or by reference). Absent means billed
3249
+ * like any other call. Set it only from the provider's own response.
3250
+ */
3251
+ unbilledRefusal?: boolean;
3192
3252
  /**
3193
3253
  * Llamadas a herramienta que pidió el modelo, tal cual las devolvió el
3194
3254
  * proveedor.
@@ -3477,7 +3537,28 @@ interface SavingsLayerConfig {
3477
3537
  provider: Provider;
3478
3538
  tokenizer: TokenizerRegistry;
3479
3539
  exactCache?: ExactCache;
3540
+ /**
3541
+ * WHERE reworded questions are looked up and stored. Giving the layer a cache
3542
+ * does NOT switch it on: a request still has to ask for it with
3543
+ * `savings.semanticCache`, or you set {@link semanticCacheDefault} to ask for
3544
+ * it on every request. A layer that has a cache and a request that neither
3545
+ * asks for it nor turns it off gets a one-time console warning (silenced by
3546
+ * `silenceWarnings`, or by setting `semanticCacheDefault`, `false` included).
3547
+ */
3480
3548
  semanticCache?: InMemorySemanticCache;
3549
+ /**
3550
+ * Default for `savings.semanticCache` on every request of this layer. A
3551
+ * request's own value always wins, `false` included. Unset, the semantic cache
3552
+ * is used only by requests that ask for it.
3553
+ *
3554
+ * OPT-IN, and deliberately a separate field from {@link semanticCache}: the
3555
+ * cache is the backend, this is the decision to serve answers to reworded
3556
+ * questions, which can serve a wrong answer. Prefer it with a cache built with
3557
+ * `calibration`, which serves nothing until your own traffic certifies a
3558
+ * floor. `false` states that you switch it on per request, and silences the
3559
+ * warning about a cache that no request uses.
3560
+ */
3561
+ semanticCacheDefault?: boolean | "safe-only" | "include-agentic";
3481
3562
  singleflight?: Singleflight;
3482
3563
  resolvers?: ResolverRegistry;
3483
3564
  router?: ExecutionRouter;
@@ -3777,6 +3858,17 @@ interface SavingsLayerConfig {
3777
3858
  * it when you have your own observability. Applies to the pipeline, the usage
3778
3859
  * emitter, the revocation poller and the provider adapters shipped in this
3779
3860
  * package alike.
3861
+ *
3862
+ * **One warning is NOT silenced: a rejected renewal credential.** When the
3863
+ * automatic pickup of the renewed license gets HTTP 401, 403 or 404 (the
3864
+ * `serviceAccountKey` was rotated, belongs to another organization, or
3865
+ * `licenseServerUrl` is wrong), the SDK warns even with this flag on. That is
3866
+ * not a degraded saving: the token will not renew and the layer will stop
3867
+ * operating when the grace period ends, and the failure happens inside the SDK
3868
+ * where your own observability cannot see it. It is printed once per status
3869
+ * code per layer, never on every renewal attempt, and it never contains the
3870
+ * key or any part of it. Fix the credential, or turn the pickup off with
3871
+ * {@link licenseRefresh}`.enabled = false`, to make it go away.
3780
3872
  */
3781
3873
  silenceWarnings?: boolean;
3782
3874
  /**
@@ -4549,6 +4641,25 @@ interface OpenAICompatibleOptions {
4549
4641
  * saving. Benchmarks that make real cost claims must set this.
4550
4642
  */
4551
4643
  requireUsage?: boolean;
4644
+ /**
4645
+ * Which Chat Completions field carries the output cap (`maxOutputTokens`).
4646
+ *
4647
+ * Default (undefined): `max_completion_tokens` when `baseUrl` is OpenAI's own
4648
+ * API (https://api.openai.com), `max_tokens` everywhere else. OpenAI
4649
+ * deprecated `max_tokens` when it introduced `max_completion_tokens`
4650
+ * (2024-09-12) and its reasoning models (o-series, gpt-5.x) reject it with a
4651
+ * 400; `max_completion_tokens` is accepted by every current OpenAI chat
4652
+ * model. OpenAI-compatible servers (proxies, self-hosted inference servers,
4653
+ * routers, Azure with an older api-version, a local gateway) understand
4654
+ * `max_tokens`, and some silently IGNORE a field they do not know, which
4655
+ * would drop the cap without an error.
4656
+ *
4657
+ * With the default, a 400/422 that names the field sent as unsupported is
4658
+ * retried once with the other one (it is rejected before any token is
4659
+ * generated, so nothing is billed twice) and the model is remembered. An
4660
+ * explicit value always wins and is never switched.
4661
+ */
4662
+ maxTokensParam?: "max_tokens" | "max_completion_tokens";
4552
4663
  }
4553
4664
  /**
4554
4665
  * ¿Cumple este esquema las reglas del modo `strict` de OpenAI?
@@ -4578,6 +4689,7 @@ declare class OpenAICompatibleProvider implements Provider {
4578
4689
  /** Models of this endpoint already known to reject `response_format`. */
4579
4690
  private readonly sinResponseFormat;
4580
4691
  private streamOptionsRejected;
4692
+ private readonly campoDeTopeAprendido;
4581
4693
  private readonly origen;
4582
4694
  constructor(opts: OpenAICompatibleOptions);
4583
4695
  generate(req: ProviderRequest, options?: ProviderStreamOptions): Promise<ProviderResponse>;
@@ -4591,6 +4703,8 @@ declare class OpenAICompatibleProvider implements Provider {
4591
4703
  * as one text event.
4592
4704
  */
4593
4705
  generateStream(req: ProviderRequest, options?: ProviderStreamOptions): AsyncGenerator<ProviderStreamEvent, void, undefined>;
4706
+ private campoDeTope;
4707
+ private corregirCampoDeTope;
4594
4708
  private enviar;
4595
4709
  private construirCuerpo;
4596
4710
  /** Translate a 2xx endpoint response into the provider-neutral shape. */