@rulvar/openai 1.19.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import OpenAI, { ClientOptions } from "openai";
2
- import { CanonicalId, ChatEvent, ChatRequest, Effort, ModelCaps, PriceTable, ProviderAdapter, Usage, WireError } from "@rulvar/core";
2
+ import { CanonicalId, ChatEvent, ChatRequest, Effort, JournalEntry, ModelCaps, ModelRef, PriceTable, ProviderAdapter, Usage, WireError } from "@rulvar/core";
3
3
 
4
4
  //#region src/caps.d.ts
5
5
  interface OpenAiModelInfo {
@@ -8,11 +8,13 @@ interface OpenAiModelInfo {
8
8
  /** Reasoning models reject non-default sampling parameters. */
9
9
  reasoning: boolean;
10
10
  /**
11
- * The model accepts wire `reasoning.effort: "max"` (GPT-5.6 Sol per
12
- * the official model docs). When false, canonical max downmaps to
13
- * wire xhigh; the downmap is recorded in providerMetadata and the
14
- * journal identity keeps max, so caps accept the full canonical set
15
- * either way.
11
+ * The model accepts wire `reasoning.effort: "max"` (the whole GPT-5.6
12
+ * family per the official model guidance, each sibling verified live
13
+ * 2026-07-18). When false, canonical max downmaps to wire xhigh; the
14
+ * downmap is recorded in providerMetadata and the journal identity
15
+ * keeps max, so caps accept the full canonical set either way. Flip
16
+ * this to true ONLY on a per-model live verification, never from the
17
+ * family page alone.
16
18
  */
17
19
  wireMaxEffort: boolean;
18
20
  }
@@ -108,6 +110,41 @@ interface OpenAiCompatibleConfig {
108
110
  /** Creates a Chat Completions dialect adapter for a compatible endpoint. */
109
111
  declare function openaiCompatible(cfg: OpenAiCompatibleConfig): ProviderAdapter;
110
112
  //#endregion
113
+ //#region src/audit.d.ts
114
+ /**
115
+ * The exact inverse of the v1.19.0 double count for one usage:
116
+ * subtracts `cacheWriteTokens` back out of `inputTokens` and leaves
117
+ * every other field untouched. A usage without cache writes is returned
118
+ * unchanged (v1.19.0 recorded those correctly). Throws a typed
119
+ * ConfigError when the arithmetic cannot be the v1.19.0 shape (the
120
+ * recorded input has no room for the subtraction), which means the
121
+ * usage was NOT recorded by the affected adapter; do not guess.
122
+ */
123
+ declare function undoV1190CacheDoubleCount(usage: Usage): Usage;
124
+ /** One journal's sidecar reconciliation; see auditV1190CacheJournal. */
125
+ interface V1190CacheAudit {
126
+ /** Entries whose usage carried the affected shape and were inverted. */
127
+ affectedEntries: number;
128
+ /** The fold as recorded (what reports and budgets saw). */
129
+ recordedUsd: number;
130
+ /** The fold with every affected usage inverted to the true wire shape. */
131
+ correctedUsd: number;
132
+ }
133
+ /**
134
+ * Folds a journal twice with the SAME price function: once as recorded
135
+ * and once with every affected OpenAI usage passed through
136
+ * `undoV1190CacheDoubleCount`, returning both totals and the affected
137
+ * entry count. An entry (or per-model slice) counts as affected when it
138
+ * was served by the `openai` adapter, carries cache writes, and has no
139
+ * `usageSemantics` stamp; stamped entries are already correct and fold
140
+ * identically in both totals. The journal itself is never touched.
141
+ * `recordedUsd - correctedUsd` is the exact overcharge IF the journal
142
+ * was recorded by v1.19.0; for a v1.20.0 journal the same shape folds
143
+ * to a smaller `correctedUsd` that does NOT correspond to any real
144
+ * charge, so version provenance stays the caller's responsibility.
145
+ */
146
+ declare function auditV1190CacheJournal(entries: readonly JournalEntry[], priceUsd: (servedBy: ModelRef, usage: Usage) => number | undefined): V1190CacheAudit;
147
+ //#endregion
111
148
  //#region src/wire.d.ts
112
149
  /** Bijective canonical-to-wire (call_*) id map. */
113
150
  declare class OpenAiIdMap {
@@ -121,9 +158,10 @@ declare class OpenAiIdMap {
121
158
  /**
122
159
  * Canonical-to-wire effort: low through xhigh pass through. Canonical
123
160
  * max passes through unchanged on models whose caps declare wire max
124
- * support (GPT-5.6 Sol); elsewhere it downmaps to xhigh (documented
125
- * lossy; recorded in providerMetadata). Provider 'none' is reachable
126
- * only via providerOptions.openai.reasoningEffort.
161
+ * support (the whole GPT-5.6 family, each sibling verified live
162
+ * 2026-07-18; v1.20.0 review P2-3); elsewhere it downmaps to xhigh
163
+ * (documented lossy; recorded in providerMetadata). Provider 'none' is
164
+ * reachable only via providerOptions.openai.reasoningEffort.
127
165
  */
128
166
  declare function mapOpenAiEffort(effort: Effort, options?: {
129
167
  wireMaxEffort?: boolean;
@@ -150,14 +188,29 @@ type ResponsesStreamEvent = Record<string, unknown> & {
150
188
  };
151
189
  /**
152
190
  * Normalizes Responses usage into the canonical Usage invariant, where
153
- * `inputTokens` is the FULL prompt: wire `input_tokens` already includes
154
- * cached READS, while cache WRITE tokens arrive SEPARATELY in
155
- * `input_tokens_details.cache_write_tokens` (GPT-5.6 and later families;
156
- * they bill at the 1.25x write premium, and earlier families report no
157
- * field and pay no premium). Dropping the field lost the whole write
158
- * charge and weakened the budget guard (v1.18.0 review P1-2), so writes
159
- * are added into `inputTokens` and surfaced as `cacheWriteTokens` for
160
- * the premium rate, exactly mirroring the Anthropic adapter's mapping.
191
+ * `inputTokens` is the FULL prompt. On the OpenAI wire `input_tokens`
192
+ * is ALREADY that full count: `input_tokens_details.cached_tokens` and
193
+ * `input_tokens_details.cache_write_tokens` (GPT-5.6 and later
194
+ * families) are priced SUBSETS of it, never additional tokens, so both
195
+ * pass through untouched and nothing is added. Verified on the live
196
+ * wire 2026-07-18: two identical long prompts report the SAME
197
+ * `input_tokens` while the details flip from write to read, and
198
+ * `total_tokens` equals `input_tokens + output_tokens` on both calls.
199
+ * Adding writes on top (the v1.19.0 reading of the field) double-billed
200
+ * every written token at 1x + 1.25x and inflated budget debits
201
+ * (v1.19.0 review P1-1). Contrast with the Anthropic adapter, whose
202
+ * wire genuinely EXCLUDES both cache counts from `input_tokens`, so
203
+ * that adapter adds them; the two wires differ, the canonical Usage
204
+ * invariant does not.
205
+ *
206
+ * Numeric hygiene is deliberately NOT this function's job: any `number`
207
+ * the wire (or an injected client) reports passes through, and the core
208
+ * enforces the full telemetry invariant at the adapter boundary for
209
+ * every adapter uniformly, failing the call loud on non-finite,
210
+ * negative, or fractional counts while accounting only sanitized values
211
+ * (`usageViolations`/`sanitizeUsage` in @rulvar/core; v1.20.0 review
212
+ * P1-1). Real wires report whole nonnegative integers; a violation here
213
+ * means a broken transport, never plausible provider data.
161
214
  */
162
215
  declare function normalizeOpenAiUsage(raw: Record<string, unknown> | undefined): Usage;
163
216
  /**
@@ -189,4 +242,4 @@ declare function buildChatCompletionsParams(req: ChatRequest, ids: OpenAiIdMap):
189
242
  */
190
243
  declare function mapChatCompletionsStream(stream: AsyncIterable<Record<string, unknown>>, ids: OpenAiIdMap): AsyncGenerator<ChatEvent, void>;
191
244
  //#endregion
192
- export { CONSERVATIVE_COMPATIBLE_CAPS, OPENAI_MODELS, OPENAI_PRICING, type OpenAiAdapterOptions, type OpenAiClientLike, type OpenAiCompatibleConfig, OpenAiIdMap, type OpenAiModelInfo, type OpenAiSdkOptions, type ResponsesStreamEvent, buildChatCompletionsParams, buildResponsesParams, mapChatCompletionsStream, mapOpenAiEffort, mapResponsesStream, normalizeOpenAiUsage, openAiErrorToWire, openAiModelInfo, openai, openaiCompatible };
245
+ export { CONSERVATIVE_COMPATIBLE_CAPS, OPENAI_MODELS, OPENAI_PRICING, type OpenAiAdapterOptions, type OpenAiClientLike, type OpenAiCompatibleConfig, OpenAiIdMap, type OpenAiModelInfo, type OpenAiSdkOptions, type ResponsesStreamEvent, type V1190CacheAudit, auditV1190CacheJournal, buildChatCompletionsParams, buildResponsesParams, mapChatCompletionsStream, mapOpenAiEffort, mapResponsesStream, normalizeOpenAiUsage, openAiErrorToWire, openAiModelInfo, openai, openaiCompatible, undoV1190CacheDoubleCount };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import OpenAI from "openai";
2
- import { ConfigError, createCanonicalIdMinter, isStrictCompatibleSchema } from "@rulvar/core";
2
+ import { ConfigError, createCanonicalIdMinter, entryUsageSlices, isStrictCompatibleSchema } from "@rulvar/core";
3
3
  //#region src/caps.ts
4
4
  const REASONING_EFFORTS = [
5
5
  "low",
@@ -34,7 +34,13 @@ const GPT_56_TIERS = [{
34
34
  * .../gpt-5.6-terra, .../gpt-5.6-luna; rates verified 2026-07-18). All
35
35
  * three: prompts strictly above 272K input tokens price the FULL
36
36
  * request at 2x input and 1.5x output; cache writes bill at 1.25x
37
- * uncached input. Only Sol accepts wire reasoning effort `max`.
37
+ * uncached input. All three accept wire reasoning effort `max`
38
+ * (v1.20.0 review P2-3; verified live per sibling 2026-07-18: a
39
+ * Responses call with `reasoning.effort: "max"` returns HTTP 200 with
40
+ * the effort echoed on Terra and Luna alike, and the API's own 400
41
+ * validator for an invalid effort enumerates `max` among the supported
42
+ * values, so acceptance is enforcement, not silence). Earlier families
43
+ * keep the conservative downmap until verified the same way.
38
44
  */
39
45
  const GPT_56_SOL = responses(105e4, 128e3, {
40
46
  inputUsdPerMTok: 5,
@@ -52,14 +58,14 @@ const OPENAI_MODELS = {
52
58
  cacheReadUsdPerMTok: .25,
53
59
  cacheWriteUsdPerMTok: 3.125,
54
60
  tiers: GPT_56_TIERS
55
- }),
61
+ }, { wireMaxEffort: true }),
56
62
  "gpt-5.6-luna": responses(105e4, 128e3, {
57
63
  inputUsdPerMTok: 1,
58
64
  outputUsdPerMTok: 6,
59
65
  cacheReadUsdPerMTok: .1,
60
66
  cacheWriteUsdPerMTok: 1.25,
61
67
  tiers: GPT_56_TIERS
62
- }),
68
+ }, { wireMaxEffort: true }),
63
69
  "gpt-5.6": GPT_56_SOL,
64
70
  "gpt-5.5": responses(4e5, 128e3, {
65
71
  inputUsdPerMTok: 5,
@@ -168,9 +174,10 @@ var OpenAiIdMap = class {
168
174
  /**
169
175
  * Canonical-to-wire effort: low through xhigh pass through. Canonical
170
176
  * max passes through unchanged on models whose caps declare wire max
171
- * support (GPT-5.6 Sol); elsewhere it downmaps to xhigh (documented
172
- * lossy; recorded in providerMetadata). Provider 'none' is reachable
173
- * only via providerOptions.openai.reasoningEffort.
177
+ * support (the whole GPT-5.6 family, each sibling verified live
178
+ * 2026-07-18; v1.20.0 review P2-3); elsewhere it downmaps to xhigh
179
+ * (documented lossy; recorded in providerMetadata). Provider 'none' is
180
+ * reachable only via providerOptions.openai.reasoningEffort.
174
181
  */
175
182
  function mapOpenAiEffort(effort, options) {
176
183
  if (effort === "max" && options?.wireMaxEffort !== true) return {
@@ -298,25 +305,58 @@ function buildResponsesParams(req, ids, options) {
298
305
  };
299
306
  }
300
307
  /**
308
+ * Clamps the cache detail counts into the subset domain the pricing
309
+ * fold relies on (`reads + writes <= input`, everything nonnegative).
310
+ * The provider contract already guarantees it; impossible telemetry is
311
+ * clamped rather than rejected because a rejection would discard PAID
312
+ * evidence, and clamping is the conservative direction for the budget:
313
+ * dropped detail tokens price at the FULL input rate instead of the
314
+ * cache-read discount. Reads keep priority over writes, so a violation
315
+ * shrinks the write premium, never the base charge.
316
+ */
317
+ function clampCacheSubsets(inputTokens, rawRead, rawWrite) {
318
+ const cacheReadTokens = Math.min(Math.max(0, rawRead), Math.max(0, inputTokens));
319
+ return {
320
+ cacheReadTokens,
321
+ cacheWriteTokens: Math.min(Math.max(0, rawWrite), Math.max(0, inputTokens) - cacheReadTokens)
322
+ };
323
+ }
324
+ /**
301
325
  * Normalizes Responses usage into the canonical Usage invariant, where
302
- * `inputTokens` is the FULL prompt: wire `input_tokens` already includes
303
- * cached READS, while cache WRITE tokens arrive SEPARATELY in
304
- * `input_tokens_details.cache_write_tokens` (GPT-5.6 and later families;
305
- * they bill at the 1.25x write premium, and earlier families report no
306
- * field and pay no premium). Dropping the field lost the whole write
307
- * charge and weakened the budget guard (v1.18.0 review P1-2), so writes
308
- * are added into `inputTokens` and surfaced as `cacheWriteTokens` for
309
- * the premium rate, exactly mirroring the Anthropic adapter's mapping.
326
+ * `inputTokens` is the FULL prompt. On the OpenAI wire `input_tokens`
327
+ * is ALREADY that full count: `input_tokens_details.cached_tokens` and
328
+ * `input_tokens_details.cache_write_tokens` (GPT-5.6 and later
329
+ * families) are priced SUBSETS of it, never additional tokens, so both
330
+ * pass through untouched and nothing is added. Verified on the live
331
+ * wire 2026-07-18: two identical long prompts report the SAME
332
+ * `input_tokens` while the details flip from write to read, and
333
+ * `total_tokens` equals `input_tokens + output_tokens` on both calls.
334
+ * Adding writes on top (the v1.19.0 reading of the field) double-billed
335
+ * every written token at 1x + 1.25x and inflated budget debits
336
+ * (v1.19.0 review P1-1). Contrast with the Anthropic adapter, whose
337
+ * wire genuinely EXCLUDES both cache counts from `input_tokens`, so
338
+ * that adapter adds them; the two wires differ, the canonical Usage
339
+ * invariant does not.
340
+ *
341
+ * Numeric hygiene is deliberately NOT this function's job: any `number`
342
+ * the wire (or an injected client) reports passes through, and the core
343
+ * enforces the full telemetry invariant at the adapter boundary for
344
+ * every adapter uniformly, failing the call loud on non-finite,
345
+ * negative, or fractional counts while accounting only sanitized values
346
+ * (`usageViolations`/`sanitizeUsage` in @rulvar/core; v1.20.0 review
347
+ * P1-1). Real wires report whole nonnegative integers; a violation here
348
+ * means a broken transport, never plausible provider data.
310
349
  */
311
350
  function normalizeOpenAiUsage(raw) {
312
351
  const inputDetails = raw?.input_tokens_details;
313
352
  const outputDetails = raw?.output_tokens_details;
314
- const cacheWrite = typeof inputDetails?.cache_write_tokens === "number" ? inputDetails.cache_write_tokens : 0;
353
+ const inputTokens = typeof raw?.input_tokens === "number" ? raw.input_tokens : 0;
354
+ const clamped = clampCacheSubsets(inputTokens, typeof inputDetails?.cached_tokens === "number" ? inputDetails.cached_tokens : 0, typeof inputDetails?.cache_write_tokens === "number" ? inputDetails.cache_write_tokens : 0);
315
355
  const usage = {
316
- inputTokens: (typeof raw?.input_tokens === "number" ? raw.input_tokens : 0) + cacheWrite,
356
+ inputTokens,
317
357
  outputTokens: typeof raw?.output_tokens === "number" ? raw.output_tokens : 0,
318
- cacheReadTokens: typeof inputDetails?.cached_tokens === "number" ? inputDetails.cached_tokens : 0,
319
- cacheWriteTokens: cacheWrite
358
+ cacheReadTokens: clamped.cacheReadTokens,
359
+ cacheWriteTokens: clamped.cacheWriteTokens
320
360
  };
321
361
  const reasoning = outputDetails?.reasoning_tokens;
322
362
  if (typeof reasoning === "number" && reasoning > 0) usage.reasoningTokens = reasoning;
@@ -644,12 +684,13 @@ async function* mapChatCompletionsStream(stream, ids) {
644
684
  const chunkUsage = chunk.usage;
645
685
  if (chunkUsage !== void 0 && chunkUsage !== null) {
646
686
  const promptDetails = chunkUsage.prompt_tokens_details;
647
- const cacheWrite = typeof promptDetails?.cache_write_tokens === "number" ? promptDetails.cache_write_tokens : 0;
687
+ const promptTokens = typeof chunkUsage.prompt_tokens === "number" ? chunkUsage.prompt_tokens : 0;
688
+ const clamped = clampCacheSubsets(promptTokens, typeof promptDetails?.cached_tokens === "number" ? promptDetails.cached_tokens : 0, typeof promptDetails?.cache_write_tokens === "number" ? promptDetails.cache_write_tokens : 0);
648
689
  usage = {
649
- inputTokens: (typeof chunkUsage.prompt_tokens === "number" ? chunkUsage.prompt_tokens : 0) + cacheWrite,
690
+ inputTokens: promptTokens,
650
691
  outputTokens: typeof chunkUsage.completion_tokens === "number" ? chunkUsage.completion_tokens : 0,
651
- cacheReadTokens: typeof promptDetails?.cached_tokens === "number" ? promptDetails.cached_tokens : 0,
652
- cacheWriteTokens: cacheWrite
692
+ cacheReadTokens: clamped.cacheReadTokens,
693
+ cacheWriteTokens: clamped.cacheWriteTokens
653
694
  };
654
695
  }
655
696
  }
@@ -715,6 +756,7 @@ function openai(options = {}) {
715
756
  return {
716
757
  id: "openai",
717
758
  provider: "openai",
759
+ usageSemantics: "openai-cache-subsets-v2",
718
760
  caps(model) {
719
761
  return openAiModelInfo(model).caps;
720
762
  },
@@ -785,6 +827,7 @@ function openaiCompatible(cfg) {
785
827
  return {
786
828
  id: cfg.id,
787
829
  provider: "openai",
830
+ usageSemantics: "openai-cache-subsets-v2",
788
831
  caps(model) {
789
832
  const overrides = cfg.caps?.(model);
790
833
  return overrides === void 0 ? CONSERVATIVE_COMPATIBLE_CAPS : {
@@ -810,4 +853,69 @@ function openaiCompatible(cfg) {
810
853
  };
811
854
  }
812
855
  //#endregion
813
- export { CONSERVATIVE_COMPATIBLE_CAPS, OPENAI_MODELS, OPENAI_PRICING, OpenAiIdMap, buildChatCompletionsParams, buildResponsesParams, mapChatCompletionsStream, mapOpenAiEffort, mapResponsesStream, normalizeOpenAiUsage, openAiErrorToWire, openAiModelInfo, openai, openaiCompatible };
856
+ //#region src/audit.ts
857
+ /**
858
+ * The exact inverse of the v1.19.0 double count for one usage:
859
+ * subtracts `cacheWriteTokens` back out of `inputTokens` and leaves
860
+ * every other field untouched. A usage without cache writes is returned
861
+ * unchanged (v1.19.0 recorded those correctly). Throws a typed
862
+ * ConfigError when the arithmetic cannot be the v1.19.0 shape (the
863
+ * recorded input has no room for the subtraction), which means the
864
+ * usage was NOT recorded by the affected adapter; do not guess.
865
+ */
866
+ function undoV1190CacheDoubleCount(usage) {
867
+ if (usage.cacheWriteTokens <= 0) return usage;
868
+ const trueInput = usage.inputTokens - usage.cacheWriteTokens;
869
+ if (trueInput < usage.cacheReadTokens + usage.cacheWriteTokens) throw new ConfigError(`not a v1.19.0-inflated usage: subtracting cacheWriteTokens (${String(usage.cacheWriteTokens)}) out of inputTokens (${String(usage.inputTokens)}) leaves less than the cache subsets; this usage was not recorded by the affected adapter`);
870
+ return {
871
+ ...usage,
872
+ inputTokens: trueInput
873
+ };
874
+ }
875
+ /**
876
+ * Folds a journal twice with the SAME price function: once as recorded
877
+ * and once with every affected OpenAI usage passed through
878
+ * `undoV1190CacheDoubleCount`, returning both totals and the affected
879
+ * entry count. An entry (or per-model slice) counts as affected when it
880
+ * was served by the `openai` adapter, carries cache writes, and has no
881
+ * `usageSemantics` stamp; stamped entries are already correct and fold
882
+ * identically in both totals. The journal itself is never touched.
883
+ * `recordedUsd - correctedUsd` is the exact overcharge IF the journal
884
+ * was recorded by v1.19.0; for a v1.20.0 journal the same shape folds
885
+ * to a smaller `correctedUsd` that does NOT correspond to any real
886
+ * charge, so version provenance stays the caller's responsibility.
887
+ */
888
+ function auditV1190CacheJournal(entries, priceUsd) {
889
+ let affectedEntries = 0;
890
+ let recordedUsd = 0;
891
+ let correctedUsd = 0;
892
+ for (const entry of entries) {
893
+ let entryAffected = false;
894
+ for (const slice of entryUsageSlices(entry)) {
895
+ const recorded = priceUsd(slice.servedBy, slice.usage);
896
+ if (recorded === void 0) continue;
897
+ recordedUsd += recorded;
898
+ if (entry.usageSemantics === void 0 && slice.servedBy.startsWith("openai:") && slice.usage.cacheWriteTokens > 0) {
899
+ let corrected;
900
+ try {
901
+ corrected = undoV1190CacheDoubleCount(slice.usage);
902
+ } catch {
903
+ corrected = void 0;
904
+ }
905
+ if (corrected === void 0) correctedUsd += recorded;
906
+ else {
907
+ entryAffected = true;
908
+ correctedUsd += priceUsd(slice.servedBy, corrected) ?? 0;
909
+ }
910
+ } else correctedUsd += recorded;
911
+ }
912
+ if (entryAffected) affectedEntries += 1;
913
+ }
914
+ return {
915
+ affectedEntries,
916
+ recordedUsd,
917
+ correctedUsd
918
+ };
919
+ }
920
+ //#endregion
921
+ export { CONSERVATIVE_COMPATIBLE_CAPS, OPENAI_MODELS, OPENAI_PRICING, OpenAiIdMap, auditV1190CacheJournal, buildChatCompletionsParams, buildResponsesParams, mapChatCompletionsStream, mapOpenAiEffort, mapResponsesStream, normalizeOpenAiUsage, openAiErrorToWire, openAiModelInfo, openai, openaiCompatible, undoV1190CacheDoubleCount };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/openai",
3
- "version": "1.19.0",
3
+ "version": "1.21.0",
4
4
  "description": "Rulvar first-class provider adapter for the OpenAI Responses API, plus the openaiCompatible factory.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -23,13 +23,13 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "openai": "^6.45.0",
26
- "@rulvar/core": "1.19.0"
26
+ "@rulvar/core": "1.21.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^22.20.0",
30
30
  "tsdown": "^0.22.3",
31
31
  "typescript": "~6.0.3",
32
- "@rulvar/testing": "1.19.0"
32
+ "@rulvar/testing": "1.21.0"
33
33
  },
34
34
  "repository": {
35
35
  "type": "git",