@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 +17 -1
- package/dist/index.js +97 -33
- package/package.json +3 -3
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
|
-
/**
|
|
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:
|
|
66
|
-
outputUsdPerMTok:
|
|
67
|
-
cacheReadUsdPerMTok:
|
|
65
|
+
inputUsdPerMTok: 5,
|
|
66
|
+
outputUsdPerMTok: 30,
|
|
67
|
+
cacheReadUsdPerMTok: .5
|
|
68
68
|
}),
|
|
69
69
|
"gpt-5.5-pro": responses(4e5, 128e3, {
|
|
70
|
-
inputUsdPerMTok:
|
|
71
|
-
outputUsdPerMTok:
|
|
72
|
-
cacheReadUsdPerMTok: 4
|
|
70
|
+
inputUsdPerMTok: 30,
|
|
71
|
+
outputUsdPerMTok: 180
|
|
73
72
|
}),
|
|
74
73
|
"gpt-5.4": responses(272e3, 1e5, {
|
|
75
|
-
inputUsdPerMTok:
|
|
76
|
-
outputUsdPerMTok:
|
|
77
|
-
cacheReadUsdPerMTok: .
|
|
74
|
+
inputUsdPerMTok: 2.5,
|
|
75
|
+
outputUsdPerMTok: 15,
|
|
76
|
+
cacheReadUsdPerMTok: .25
|
|
78
77
|
}),
|
|
79
78
|
"gpt-5.4-mini": responses(272e3, 1e5, {
|
|
80
|
-
inputUsdPerMTok:
|
|
81
|
-
outputUsdPerMTok: 4.
|
|
82
|
-
cacheReadUsdPerMTok: .
|
|
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
|
-
/**
|
|
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
|
|
340
|
+
inputTokens,
|
|
307
341
|
outputTokens: typeof raw?.output_tokens === "number" ? raw.output_tokens : 0,
|
|
308
|
-
cacheReadTokens:
|
|
309
|
-
cacheWriteTokens:
|
|
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:
|
|
674
|
+
inputTokens: promptTokens,
|
|
611
675
|
outputTokens: typeof chunkUsage.completion_tokens === "number" ? chunkUsage.completion_tokens : 0,
|
|
612
|
-
cacheReadTokens:
|
|
613
|
-
cacheWriteTokens:
|
|
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.
|
|
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.
|
|
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.
|
|
32
|
+
"@rulvar/testing": "1.20.0"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|