@rulvar/openai 1.18.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
@@ -148,7 +148,23 @@ declare function buildResponsesParams(req: ChatRequest, ids: OpenAiIdMap, option
148
148
  type ResponsesStreamEvent = Record<string, unknown> & {
149
149
  type: string;
150
150
  };
151
- /** Normalizes Responses usage: input_tokens already includes cached reads. */
151
+ /**
152
+ * Normalizes Responses usage into the canonical Usage invariant, where
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.
167
+ */
152
168
  declare function normalizeOpenAiUsage(raw: Record<string, unknown> | undefined): Usage;
153
169
  /**
154
170
  * Maps the typed Responses SSE stream to ChatEvents, yielding each
package/dist/index.js CHANGED
@@ -62,24 +62,23 @@ const OPENAI_MODELS = {
62
62
  }),
63
63
  "gpt-5.6": GPT_56_SOL,
64
64
  "gpt-5.5": responses(4e5, 128e3, {
65
- inputUsdPerMTok: 10,
66
- outputUsdPerMTok: 40,
67
- cacheReadUsdPerMTok: 1
65
+ inputUsdPerMTok: 5,
66
+ outputUsdPerMTok: 30,
67
+ cacheReadUsdPerMTok: .5
68
68
  }),
69
69
  "gpt-5.5-pro": responses(4e5, 128e3, {
70
- inputUsdPerMTok: 40,
71
- outputUsdPerMTok: 160,
72
- cacheReadUsdPerMTok: 4
70
+ inputUsdPerMTok: 30,
71
+ outputUsdPerMTok: 180
73
72
  }),
74
73
  "gpt-5.4": responses(272e3, 1e5, {
75
- inputUsdPerMTok: 6,
76
- outputUsdPerMTok: 24,
77
- cacheReadUsdPerMTok: .6
74
+ inputUsdPerMTok: 2.5,
75
+ outputUsdPerMTok: 15,
76
+ cacheReadUsdPerMTok: .25
78
77
  }),
79
78
  "gpt-5.4-mini": responses(272e3, 1e5, {
80
- inputUsdPerMTok: 1.2,
81
- outputUsdPerMTok: 4.8,
82
- cacheReadUsdPerMTok: .12
79
+ inputUsdPerMTok: .75,
80
+ outputUsdPerMTok: 4.5,
81
+ cacheReadUsdPerMTok: .075
83
82
  })
84
83
  };
85
84
  /**
@@ -102,7 +101,7 @@ const OPENAI_MODELS = {
102
101
  * silent reinterpretation.
103
102
  */
104
103
  const OPENAI_PRICING = {
105
- pricingVersion: "openai-2026-07-18",
104
+ pricingVersion: "openai-2026-07-18-r2",
106
105
  models: (() => {
107
106
  const models = {};
108
107
  for (const [name, info] of Object.entries(OPENAI_MODELS)) if (info.caps.pricing !== void 0) models[`openai:${name}`] = info.caps.pricing;
@@ -298,15 +297,50 @@ function buildResponsesParams(req, ids, options) {
298
297
  effortDownmapped
299
298
  };
300
299
  }
301
- /** Normalizes Responses usage: input_tokens already includes cached reads. */
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
+ /**
318
+ * Normalizes Responses usage into the canonical Usage invariant, where
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.
333
+ */
302
334
  function normalizeOpenAiUsage(raw) {
303
335
  const inputDetails = raw?.input_tokens_details;
304
336
  const outputDetails = raw?.output_tokens_details;
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);
305
339
  const usage = {
306
- inputTokens: typeof raw?.input_tokens === "number" ? raw.input_tokens : 0,
340
+ inputTokens,
307
341
  outputTokens: typeof raw?.output_tokens === "number" ? raw.output_tokens : 0,
308
- cacheReadTokens: typeof inputDetails?.cached_tokens === "number" ? inputDetails.cached_tokens : 0,
309
- cacheWriteTokens: 0
342
+ cacheReadTokens: clamped.cacheReadTokens,
343
+ cacheWriteTokens: clamped.cacheWriteTokens
310
344
  };
311
345
  const reasoning = outputDetails?.reasoning_tokens;
312
346
  if (typeof reasoning === "number" && reasoning > 0) usage.reasoningTokens = reasoning;
@@ -415,31 +449,59 @@ async function* mapResponsesStream(stream, ids, options) {
415
449
  };
416
450
  return;
417
451
  }
418
- case "response.failed":
452
+ case "response.failed": {
453
+ const response = event.response;
454
+ const rawUsage = response?.usage;
455
+ if (rawUsage !== void 0) yield {
456
+ type: "usage",
457
+ usage: normalizeOpenAiUsage(rawUsage)
458
+ };
459
+ const error = response?.error;
419
460
  yield {
420
461
  type: "error",
421
- error: {
422
- code: "agent",
423
- message: (event.response?.error)?.message ?? "response.failed",
424
- retryable: false,
425
- data: { kind: "transport" }
426
- }
462
+ error: failedResponseError(typeof error?.code === "string" ? error.code : void 0, error?.message ?? "response.failed")
427
463
  };
428
464
  return;
465
+ }
429
466
  case "error":
430
467
  yield {
431
468
  type: "error",
432
- error: {
433
- code: "agent",
434
- message: event.message ?? "stream error",
435
- retryable: false,
436
- data: { kind: "transport" }
437
- }
469
+ error: failedResponseError(typeof event.code === "string" ? event.code : void 0, event.message ?? "stream error")
438
470
  };
439
471
  return;
440
472
  default: break;
441
473
  }
442
474
  }
475
+ /**
476
+ * Classifies a terminal stream failure (`response.failed` /
477
+ * SSE `error`) into the retryable WireError vocabulary by the
478
+ * documented `response.error.code` enum (Responses API reference):
479
+ * `rate_limit_exceeded` retries as a rate limit, `server_error` and
480
+ * timeout-class codes retry as transport faults, and everything else
481
+ * (validation such as `invalid_prompt`, policy, auth) stays
482
+ * non-retryable. Unknown codes fail closed as non-retryable: retrying a
483
+ * permanent failure would loop-bill it (v1.18.0 review P1-3).
484
+ */
485
+ function failedResponseError(code, message) {
486
+ if (code === "rate_limit_exceeded") return {
487
+ code: "agent",
488
+ message,
489
+ retryable: true,
490
+ data: {
491
+ kind: "rate-limit",
492
+ providerCode: code
493
+ }
494
+ };
495
+ return {
496
+ code: "agent",
497
+ message,
498
+ retryable: code === "server_error" || code !== void 0 && code.endsWith("_timeout"),
499
+ data: {
500
+ kind: "transport",
501
+ ...code === void 0 ? {} : { providerCode: code }
502
+ }
503
+ };
504
+ }
443
505
  /** Projects SDK/API errors into the retryable WireError vocabulary. */
444
506
  function openAiErrorToWire(error) {
445
507
  const record = error;
@@ -606,11 +668,13 @@ async function* mapChatCompletionsStream(stream, ids) {
606
668
  const chunkUsage = chunk.usage;
607
669
  if (chunkUsage !== void 0 && chunkUsage !== null) {
608
670
  const promptDetails = chunkUsage.prompt_tokens_details;
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);
609
673
  usage = {
610
- inputTokens: typeof chunkUsage.prompt_tokens === "number" ? chunkUsage.prompt_tokens : 0,
674
+ inputTokens: promptTokens,
611
675
  outputTokens: typeof chunkUsage.completion_tokens === "number" ? chunkUsage.completion_tokens : 0,
612
- cacheReadTokens: typeof promptDetails?.cached_tokens === "number" ? promptDetails.cached_tokens : 0,
613
- cacheWriteTokens: 0
676
+ cacheReadTokens: clamped.cacheReadTokens,
677
+ cacheWriteTokens: clamped.cacheWriteTokens
614
678
  };
615
679
  }
616
680
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/openai",
3
- "version": "1.18.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.18.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.18.0"
32
+ "@rulvar/testing": "1.20.0"
33
33
  },
34
34
  "repository": {
35
35
  "type": "git",