@rulvar/openai 1.19.0 → 1.20.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
@@ -150,14 +150,20 @@ type ResponsesStreamEvent = Record<string, unknown> & {
150
150
  };
151
151
  /**
152
152
  * 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.
153
+ * `inputTokens` is the FULL prompt. On the OpenAI wire `input_tokens`
154
+ * is ALREADY that full count: `input_tokens_details.cached_tokens` and
155
+ * `input_tokens_details.cache_write_tokens` (GPT-5.6 and later
156
+ * families) are priced SUBSETS of it, never additional tokens, so both
157
+ * pass through untouched and nothing is added. Verified on the live
158
+ * wire 2026-07-18: two identical long prompts report the SAME
159
+ * `input_tokens` while the details flip from write to read, and
160
+ * `total_tokens` equals `input_tokens + output_tokens` on both calls.
161
+ * Adding writes on top (the v1.19.0 reading of the field) double-billed
162
+ * every written token at 1x + 1.25x and inflated budget debits
163
+ * (v1.19.0 review P1-1). Contrast with the Anthropic adapter, whose
164
+ * wire genuinely EXCLUDES both cache counts from `input_tokens`, so
165
+ * that adapter adds them; the two wires differ, the canonical Usage
166
+ * invariant does not.
161
167
  */
162
168
  declare function normalizeOpenAiUsage(raw: Record<string, unknown> | undefined): Usage;
163
169
  /**
package/dist/index.js CHANGED
@@ -298,25 +298,49 @@ function buildResponsesParams(req, ids, options) {
298
298
  };
299
299
  }
300
300
  /**
301
+ * Clamps the cache detail counts into the subset domain the pricing
302
+ * fold relies on (`reads + writes <= input`, everything nonnegative).
303
+ * The provider contract already guarantees it; impossible telemetry is
304
+ * clamped rather than rejected because a rejection would discard PAID
305
+ * evidence, and clamping is the conservative direction for the budget:
306
+ * dropped detail tokens price at the FULL input rate instead of the
307
+ * cache-read discount. Reads keep priority over writes, so a violation
308
+ * shrinks the write premium, never the base charge.
309
+ */
310
+ function clampCacheSubsets(inputTokens, rawRead, rawWrite) {
311
+ const cacheReadTokens = Math.min(Math.max(0, rawRead), Math.max(0, inputTokens));
312
+ return {
313
+ cacheReadTokens,
314
+ cacheWriteTokens: Math.min(Math.max(0, rawWrite), Math.max(0, inputTokens) - cacheReadTokens)
315
+ };
316
+ }
317
+ /**
301
318
  * 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.
319
+ * `inputTokens` is the FULL prompt. On the OpenAI wire `input_tokens`
320
+ * is ALREADY that full count: `input_tokens_details.cached_tokens` and
321
+ * `input_tokens_details.cache_write_tokens` (GPT-5.6 and later
322
+ * families) are priced SUBSETS of it, never additional tokens, so both
323
+ * pass through untouched and nothing is added. Verified on the live
324
+ * wire 2026-07-18: two identical long prompts report the SAME
325
+ * `input_tokens` while the details flip from write to read, and
326
+ * `total_tokens` equals `input_tokens + output_tokens` on both calls.
327
+ * Adding writes on top (the v1.19.0 reading of the field) double-billed
328
+ * every written token at 1x + 1.25x and inflated budget debits
329
+ * (v1.19.0 review P1-1). Contrast with the Anthropic adapter, whose
330
+ * wire genuinely EXCLUDES both cache counts from `input_tokens`, so
331
+ * that adapter adds them; the two wires differ, the canonical Usage
332
+ * invariant does not.
310
333
  */
311
334
  function normalizeOpenAiUsage(raw) {
312
335
  const inputDetails = raw?.input_tokens_details;
313
336
  const outputDetails = raw?.output_tokens_details;
314
- const cacheWrite = typeof inputDetails?.cache_write_tokens === "number" ? inputDetails.cache_write_tokens : 0;
337
+ const inputTokens = typeof raw?.input_tokens === "number" ? raw.input_tokens : 0;
338
+ 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
339
  const usage = {
316
- inputTokens: (typeof raw?.input_tokens === "number" ? raw.input_tokens : 0) + cacheWrite,
340
+ inputTokens,
317
341
  outputTokens: typeof raw?.output_tokens === "number" ? raw.output_tokens : 0,
318
- cacheReadTokens: typeof inputDetails?.cached_tokens === "number" ? inputDetails.cached_tokens : 0,
319
- cacheWriteTokens: cacheWrite
342
+ cacheReadTokens: clamped.cacheReadTokens,
343
+ cacheWriteTokens: clamped.cacheWriteTokens
320
344
  };
321
345
  const reasoning = outputDetails?.reasoning_tokens;
322
346
  if (typeof reasoning === "number" && reasoning > 0) usage.reasoningTokens = reasoning;
@@ -644,12 +668,13 @@ async function* mapChatCompletionsStream(stream, ids) {
644
668
  const chunkUsage = chunk.usage;
645
669
  if (chunkUsage !== void 0 && chunkUsage !== null) {
646
670
  const promptDetails = chunkUsage.prompt_tokens_details;
647
- const cacheWrite = typeof promptDetails?.cache_write_tokens === "number" ? promptDetails.cache_write_tokens : 0;
671
+ const promptTokens = typeof chunkUsage.prompt_tokens === "number" ? chunkUsage.prompt_tokens : 0;
672
+ 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
673
  usage = {
649
- inputTokens: (typeof chunkUsage.prompt_tokens === "number" ? chunkUsage.prompt_tokens : 0) + cacheWrite,
674
+ inputTokens: promptTokens,
650
675
  outputTokens: typeof chunkUsage.completion_tokens === "number" ? chunkUsage.completion_tokens : 0,
651
- cacheReadTokens: typeof promptDetails?.cached_tokens === "number" ? promptDetails.cached_tokens : 0,
652
- cacheWriteTokens: cacheWrite
676
+ cacheReadTokens: clamped.cacheReadTokens,
677
+ cacheWriteTokens: clamped.cacheWriteTokens
653
678
  };
654
679
  }
655
680
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/openai",
3
- "version": "1.19.0",
3
+ "version": "1.20.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.20.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.20.0"
33
33
  },
34
34
  "repository": {
35
35
  "type": "git",