@bivelio/savings-layer 0.4.28 → 0.4.30

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.28";
328
+ declare const BV_SALA_VERSION = "0.4.30";
315
329
 
316
330
  /**
317
331
  * Environment-agnostic UUID generation.
@@ -483,7 +497,12 @@ interface ChatMessage {
483
497
  * ids included.
484
498
  */
485
499
  toolCalls?: ToolCallRequest[];
486
- /** Opaque provider metadata; never used for the exact-cache key. */
500
+ /**
501
+ * Opaque provider metadata; never used for the exact-cache key. The one
502
+ * exception is `tool_call_id` on a `role: "tool"` message: the OpenAI
503
+ * adapter still sends it as the call id when `toolCallId` is absent,
504
+ * so the key reads it as that same field.
505
+ */
487
506
  metadata?: Record<string, unknown>;
488
507
  }
489
508
  /** How a request is classified by the execution router. */
@@ -1121,6 +1140,14 @@ interface SavingsResult<T = unknown> {
1121
1140
  * (caché/resolver), que solo sirven respuestas finales completas.
1122
1141
  */
1123
1142
  finishReason?: string;
1143
+ /**
1144
+ * The refusal message the model returned instead of an answer (OpenAI
1145
+ * Structured Outputs: `message.refusal`, with `finish_reason: "stop"` and no
1146
+ * content). When present, the request was refused: `text` is empty and no
1147
+ * output contract was met. It is measured like any call, and it is never
1148
+ * repaired, cached or replayed.
1149
+ */
1150
+ refusal?: string;
1124
1151
  report: SavingsReport;
1125
1152
  }
1126
1153
  /** One event of a streamed answer. */
@@ -1296,8 +1323,9 @@ interface QualityRepairCheck {
1296
1323
  * - `decode_time_constraint`: generation was already constrained, so a retry
1297
1324
  * would not change the outcome.
1298
1325
  * - `provider_refusal`: the provider refused the request (`finishReason:
1299
- * "content_filter"`, Anthropic's `stop_reason: "refusal"`); a repair on the
1300
- * same model would be refused, and may be billed, again.
1326
+ * "content_filter"`, Anthropic's `stop_reason: "refusal"`, or OpenAI's
1327
+ * `message.refusal`); a repair on the same model would be refused, and may
1328
+ * be billed, again.
1301
1329
  */
1302
1330
  skipReason?: "output_truncated" | "schema_defect" | "would_cost_more_than_no_layer" | "decode_time_constraint" | "provider_refusal";
1303
1331
  }
@@ -1849,6 +1877,25 @@ interface CanonicalRequest {
1849
1877
  normalizedText: string;
1850
1878
  systemPromptHash: string;
1851
1879
  toolsetHash: string;
1880
+ /**
1881
+ * Hash of the tool-loop fields of the conversation, the part of each turn
1882
+ * that `normalizedText` does not carry: the calls on an assistant turn (name,
1883
+ * arguments and id) and, on a `role: "tool"` message, the call it answers,
1884
+ * its error flag and its name. Part of the exact key, the in-flight key and
1885
+ * the semantic filter, so two conversations that differ only there never
1886
+ * share an answer.
1887
+ *
1888
+ * Call ids count by what they pair, not by their value: results matched to
1889
+ * other calls give another hash, while the same conversation with its ids
1890
+ * consistently renamed (a new run of the same agent loop gets fresh ids from
1891
+ * the provider) keeps it.
1892
+ *
1893
+ * Absent when the request carries none of them: the keys are then built from
1894
+ * exactly the same material as before this field existed. A field that is
1895
+ * present but empty or false (`toolCalls: []`, `isError: false`, `name: ""`)
1896
+ * reaches the provider exactly as an absent one, and counts as absent.
1897
+ */
1898
+ toolTurnsHash?: string;
1852
1899
  /** Hash of the structured payload; part of the cache key so different data never collides. */
1853
1900
  structuredDataHash: string;
1854
1901
  schemaVersion: string;
@@ -2122,6 +2169,13 @@ interface SemanticRecord {
2122
2169
  safetyPolicyVersion: string;
2123
2170
  /** Hash of the declared tool catalog (ids, risk and parameters). */
2124
2171
  toolsetHash: string;
2172
+ /**
2173
+ * Hash of the tool-loop fields of the conversation (tool calls, the call each
2174
+ * tool result answers, its error flag and name); absent when it had none.
2175
+ * Compared for equality like the other identity fields: similarity is scored
2176
+ * on text only, and never decides between two tool loops.
2177
+ */
2178
+ toolTurnsHash?: string;
2125
2179
  /** Sampling temperature of the request, or null when it set none. */
2126
2180
  temperature: number | null;
2127
2181
  /** Sampling seed of the request, or null when it set none. */
@@ -3138,6 +3192,30 @@ interface ProviderResponse {
3138
3192
  * podía distinguirlo y hacía exactamente eso.
3139
3193
  */
3140
3194
  finishReason?: "stop" | "length" | "content_filter" | "tool_calls" | (string & {});
3195
+ /**
3196
+ * The refusal message the model returned INSTEAD of an answer, exactly as the
3197
+ * provider wrote it: OpenAI's `message.refusal` (Chat Completions with
3198
+ * Structured Outputs), which arrives with `finish_reason: "stop"` and no
3199
+ * `content`. Present only when non-empty.
3200
+ *
3201
+ * The layer treats it like `finishReason: "content_filter"`: the call is
3202
+ * measured, and the answer is never repaired against its contract, cached, or
3203
+ * replayed by reference. `text` and `finishReason` stay what the provider said.
3204
+ */
3205
+ refusal?: string;
3206
+ /**
3207
+ * True when this answer is a refusal the provider documents as NOT billed:
3208
+ * Anthropic's Messages API refusal that arrives before any output, in a
3209
+ * category its published table marks as not billed (`cyber`,
3210
+ * `general_harms`, or a `null` category). See
3211
+ * https://platform.claude.com/docs/en/build-with-claude/refusals-and-fallback
3212
+ * («How refusals are billed»).
3213
+ *
3214
+ * The layer then measures this call at cost 0 and credits no saving on the
3215
+ * request at all (measured, estimated or by reference). Absent means billed
3216
+ * like any other call. Set it only from the provider's own response.
3217
+ */
3218
+ unbilledRefusal?: boolean;
3141
3219
  /**
3142
3220
  * Llamadas a herramienta que pidió el modelo, tal cual las devolvió el
3143
3221
  * proveedor.
@@ -3724,7 +3802,19 @@ interface SavingsLayerConfig {
3724
3802
  * revocation list going stale. Nothing here throws and nothing here changes
3725
3803
  * what the layer does — turning it on only removes the console noise, so set
3726
3804
  * it when you have your own observability. Applies to the pipeline, the usage
3727
- * emitter and the revocation poller alike.
3805
+ * emitter, the revocation poller and the provider adapters shipped in this
3806
+ * package alike.
3807
+ *
3808
+ * **One warning is NOT silenced: a rejected renewal credential.** When the
3809
+ * automatic pickup of the renewed license gets HTTP 401, 403 or 404 (the
3810
+ * `serviceAccountKey` was rotated, belongs to another organization, or
3811
+ * `licenseServerUrl` is wrong), the SDK warns even with this flag on. That is
3812
+ * not a degraded saving: the token will not renew and the layer will stop
3813
+ * operating when the grace period ends, and the failure happens inside the SDK
3814
+ * where your own observability cannot see it. It is printed once per status
3815
+ * code per layer, never on every renewal attempt, and it never contains the
3816
+ * key or any part of it. Fix the credential, or turn the pickup off with
3817
+ * {@link licenseRefresh}`.enabled = false`, to make it go away.
3728
3818
  */
3729
3819
  silenceWarnings?: boolean;
3730
3820
  /**
@@ -4104,7 +4194,8 @@ declare const registrarTokenizadorExacto: typeof registerExactTokenizer;
4104
4194
  *
4105
4195
  * A hash of the user text alone is NOT a safe cache key. Two requests may share
4106
4196
  * text but differ in tenant, permission scope, model version, system prompt,
4107
- * schema, toolset, locale, sampling, knowledge epoch or safety policy — any of
4197
+ * schema, toolset, tool-loop fields (tool calls, the call a tool result answers,
4198
+ * its error flag), locale, sampling, knowledge epoch or safety policy — any of
4108
4199
  * which can change the correct answer. The exact key includes all of them.
4109
4200
  *
4110
4201
  * `servedModel` CARRIES THE MODEL THAT WILL ACTUALLY ANSWER, not the one the
@@ -4902,7 +4993,6 @@ declare class RevocationPoller {
4902
4993
  private consecutiveFailures;
4903
4994
  private stopped;
4904
4995
  private started;
4905
- private warnedNoFetch;
4906
4996
  /** ETag of the last 200; sent as If-None-Match so an unchanged list 304s (cheap no-op). */
4907
4997
  private lastEtag;
4908
4998
  /** 304 seguidos; al llegar al tope se fuerza una descarga firmada. */