@rulvar/openai 1.27.0 → 1.29.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 +19 -1
- package/dist/index.js +70 -5
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -221,9 +221,17 @@ declare function normalizeOpenAiUsage(raw: Record<string, unknown> | undefined):
|
|
|
221
221
|
* output array, never the output_text aggregate. Raw output items ride
|
|
222
222
|
* finish.providerMetadata.openai.outputItems so the runtime can retain
|
|
223
223
|
* reasoning items as provider-raw parts.
|
|
224
|
+
*
|
|
225
|
+
* A stream that drains without any response terminal event
|
|
226
|
+
* (`response.completed`, `response.incomplete`, `response.failed`, or
|
|
227
|
+
* `error`) is a truncated wire read: the mapper fails closed with one
|
|
228
|
+
* retryable transport error instead of ending silently, unless
|
|
229
|
+
* `options.signal` shows the caller requested the abort (the documented
|
|
230
|
+
* exception that ends a stream without a terminal event).
|
|
224
231
|
*/
|
|
225
232
|
declare function mapResponsesStream(stream: AsyncIterable<ResponsesStreamEvent>, ids: OpenAiIdMap, options?: {
|
|
226
233
|
effortDownmapped?: boolean;
|
|
234
|
+
signal?: AbortSignal;
|
|
227
235
|
}): AsyncGenerator<ChatEvent, void>;
|
|
228
236
|
/** Projects SDK/API errors into the retryable WireError vocabulary. */
|
|
229
237
|
declare function openAiErrorToWire(error: unknown): WireError;
|
|
@@ -239,7 +247,17 @@ declare function buildChatCompletionsParams(req: ChatRequest, ids: OpenAiIdMap):
|
|
|
239
247
|
* Delta-patched chunk assembly for the degraded path; yields each
|
|
240
248
|
* canonical event as its chunk is consumed (same live-streaming contract
|
|
241
249
|
* as mapResponsesStream).
|
|
250
|
+
*
|
|
251
|
+
* The chat dialect has no explicit terminal frame at this layer: the
|
|
252
|
+
* only completion signal is a `finish_reason` on the last choice chunk.
|
|
253
|
+
* A stream that drains without one is a truncated wire read, so the
|
|
254
|
+
* mapper fails closed with one retryable transport error (after
|
|
255
|
+
* forwarding any usage the provider did report, which was still paid
|
|
256
|
+
* for) instead of synthesizing a `stop` finish, unless `options.signal`
|
|
257
|
+
* shows the caller requested the abort.
|
|
242
258
|
*/
|
|
243
|
-
declare function mapChatCompletionsStream(stream: AsyncIterable<Record<string, unknown>>, ids: OpenAiIdMap
|
|
259
|
+
declare function mapChatCompletionsStream(stream: AsyncIterable<Record<string, unknown>>, ids: OpenAiIdMap, options?: {
|
|
260
|
+
signal?: AbortSignal;
|
|
261
|
+
}): AsyncGenerator<ChatEvent, void>;
|
|
244
262
|
//#endregion
|
|
245
263
|
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
|
@@ -370,6 +370,13 @@ function normalizeOpenAiUsage(raw) {
|
|
|
370
370
|
* output array, never the output_text aggregate. Raw output items ride
|
|
371
371
|
* finish.providerMetadata.openai.outputItems so the runtime can retain
|
|
372
372
|
* reasoning items as provider-raw parts.
|
|
373
|
+
*
|
|
374
|
+
* A stream that drains without any response terminal event
|
|
375
|
+
* (`response.completed`, `response.incomplete`, `response.failed`, or
|
|
376
|
+
* `error`) is a truncated wire read: the mapper fails closed with one
|
|
377
|
+
* retryable transport error instead of ending silently, unless
|
|
378
|
+
* `options.signal` shows the caller requested the abort (the documented
|
|
379
|
+
* exception that ends a stream without a terminal event).
|
|
373
380
|
*/
|
|
374
381
|
async function* mapResponsesStream(stream, ids, options) {
|
|
375
382
|
const callIdByItemId = /* @__PURE__ */ new Map();
|
|
@@ -487,6 +494,16 @@ async function* mapResponsesStream(stream, ids, options) {
|
|
|
487
494
|
return;
|
|
488
495
|
default: break;
|
|
489
496
|
}
|
|
497
|
+
if (options?.signal?.aborted === true) return;
|
|
498
|
+
yield {
|
|
499
|
+
type: "error",
|
|
500
|
+
error: {
|
|
501
|
+
code: "agent",
|
|
502
|
+
message: "Responses stream ended without a response terminal event (response.completed, response.incomplete, response.failed, or error); the read was truncated",
|
|
503
|
+
retryable: true,
|
|
504
|
+
data: { kind: "transport" }
|
|
505
|
+
}
|
|
506
|
+
};
|
|
490
507
|
}
|
|
491
508
|
/**
|
|
492
509
|
* Classifies a terminal stream failure (`response.failed` /
|
|
@@ -518,6 +535,24 @@ function failedResponseError(code, message) {
|
|
|
518
535
|
}
|
|
519
536
|
};
|
|
520
537
|
}
|
|
538
|
+
/**
|
|
539
|
+
* Largest delay a Node timer represents exactly; a bigger value would
|
|
540
|
+
* overflow into an almost immediate (storm inducing) retry.
|
|
541
|
+
*/
|
|
542
|
+
const MAX_RETRY_AFTER_MS = 2147483647;
|
|
543
|
+
/**
|
|
544
|
+
* Parses a Retry-After header into milliseconds. Only the delta
|
|
545
|
+
* seconds form is honored: the HTTP date form and any other
|
|
546
|
+
* unparsable value return undefined so the engine falls back to its
|
|
547
|
+
* computed policy backoff instead of receiving NaN, and a huge but
|
|
548
|
+
* finite value is clamped to the Node timer maximum (v1.28.0 review
|
|
549
|
+
* P2).
|
|
550
|
+
*/
|
|
551
|
+
function retryAfterMsFrom(header) {
|
|
552
|
+
const seconds = Number(header);
|
|
553
|
+
if (!Number.isFinite(seconds) || seconds < 0) return;
|
|
554
|
+
return Math.min(Math.round(seconds * 1e3), MAX_RETRY_AFTER_MS);
|
|
555
|
+
}
|
|
521
556
|
/** Projects SDK/API errors into the retryable WireError vocabulary. */
|
|
522
557
|
function openAiErrorToWire(error) {
|
|
523
558
|
const record = error;
|
|
@@ -528,7 +563,7 @@ function openAiErrorToWire(error) {
|
|
|
528
563
|
const headers = record.headers;
|
|
529
564
|
if (headers !== void 0 && headers !== null) {
|
|
530
565
|
const value = typeof headers.get === "function" ? headers.get("retry-after") ?? void 0 : headers["retry-after"];
|
|
531
|
-
if (value !== void 0) retryAfterMs =
|
|
566
|
+
if (value !== void 0) retryAfterMs = retryAfterMsFrom(value);
|
|
532
567
|
}
|
|
533
568
|
return {
|
|
534
569
|
code: "agent",
|
|
@@ -635,10 +670,19 @@ function buildChatCompletionsParams(req, ids) {
|
|
|
635
670
|
* Delta-patched chunk assembly for the degraded path; yields each
|
|
636
671
|
* canonical event as its chunk is consumed (same live-streaming contract
|
|
637
672
|
* as mapResponsesStream).
|
|
673
|
+
*
|
|
674
|
+
* The chat dialect has no explicit terminal frame at this layer: the
|
|
675
|
+
* only completion signal is a `finish_reason` on the last choice chunk.
|
|
676
|
+
* A stream that drains without one is a truncated wire read, so the
|
|
677
|
+
* mapper fails closed with one retryable transport error (after
|
|
678
|
+
* forwarding any usage the provider did report, which was still paid
|
|
679
|
+
* for) instead of synthesizing a `stop` finish, unless `options.signal`
|
|
680
|
+
* shows the caller requested the abort.
|
|
638
681
|
*/
|
|
639
|
-
async function* mapChatCompletionsStream(stream, ids) {
|
|
682
|
+
async function* mapChatCompletionsStream(stream, ids, options) {
|
|
640
683
|
const pendingCalls = /* @__PURE__ */ new Map();
|
|
641
684
|
let finishReason;
|
|
685
|
+
let sawUsageChunk = false;
|
|
642
686
|
let usage = {
|
|
643
687
|
inputTokens: 0,
|
|
644
688
|
outputTokens: 0,
|
|
@@ -683,6 +727,7 @@ async function* mapChatCompletionsStream(stream, ids) {
|
|
|
683
727
|
if (typeof choice?.finish_reason === "string") finishReason = choice.finish_reason;
|
|
684
728
|
const chunkUsage = chunk.usage;
|
|
685
729
|
if (chunkUsage !== void 0 && chunkUsage !== null) {
|
|
730
|
+
sawUsageChunk = true;
|
|
686
731
|
const promptDetails = chunkUsage.prompt_tokens_details;
|
|
687
732
|
const promptTokens = typeof chunkUsage.prompt_tokens === "number" ? chunkUsage.prompt_tokens : 0;
|
|
688
733
|
const clamped = clampCacheSubsets(promptTokens, typeof promptDetails?.cached_tokens === "number" ? promptDetails.cached_tokens : 0, typeof promptDetails?.cache_write_tokens === "number" ? promptDetails.cache_write_tokens : 0);
|
|
@@ -694,6 +739,23 @@ async function* mapChatCompletionsStream(stream, ids) {
|
|
|
694
739
|
};
|
|
695
740
|
}
|
|
696
741
|
}
|
|
742
|
+
if (finishReason === void 0) {
|
|
743
|
+
if (options?.signal?.aborted === true) return;
|
|
744
|
+
if (sawUsageChunk) yield {
|
|
745
|
+
type: "usage",
|
|
746
|
+
usage
|
|
747
|
+
};
|
|
748
|
+
yield {
|
|
749
|
+
type: "error",
|
|
750
|
+
error: {
|
|
751
|
+
code: "agent",
|
|
752
|
+
message: "chat completions stream ended without a finish_reason; the read was truncated",
|
|
753
|
+
retryable: true,
|
|
754
|
+
data: { kind: "transport" }
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
697
759
|
for (const [, pending] of pendingCalls) {
|
|
698
760
|
let args = {};
|
|
699
761
|
try {
|
|
@@ -768,14 +830,17 @@ function openai(options = {}) {
|
|
|
768
830
|
yield* mapResponsesStream(await client.responses.create({
|
|
769
831
|
...params,
|
|
770
832
|
stream: true
|
|
771
|
-
}, signal === void 0 ? void 0 : { signal }), ids, {
|
|
833
|
+
}, signal === void 0 ? void 0 : { signal }), ids, {
|
|
834
|
+
effortDownmapped,
|
|
835
|
+
...signal === void 0 ? {} : { signal }
|
|
836
|
+
});
|
|
772
837
|
} else {
|
|
773
838
|
const params = buildChatCompletionsParams(req, ids);
|
|
774
839
|
yield* mapChatCompletionsStream(await client.chat.completions.create({
|
|
775
840
|
...params,
|
|
776
841
|
stream: true,
|
|
777
842
|
stream_options: { include_usage: true }
|
|
778
|
-
}, signal === void 0 ? void 0 : { signal }), ids);
|
|
843
|
+
}, signal === void 0 ? void 0 : { signal }), ids, signal === void 0 ? void 0 : { signal });
|
|
779
844
|
}
|
|
780
845
|
} catch (thrown) {
|
|
781
846
|
if (signal?.aborted !== true) yield {
|
|
@@ -842,7 +907,7 @@ function openaiCompatible(cfg) {
|
|
|
842
907
|
...params,
|
|
843
908
|
stream: true,
|
|
844
909
|
stream_options: { include_usage: true }
|
|
845
|
-
}, signal === void 0 ? void 0 : { signal }), ids);
|
|
910
|
+
}, signal === void 0 ? void 0 : { signal }), ids, signal === void 0 ? void 0 : { signal });
|
|
846
911
|
} catch (thrown) {
|
|
847
912
|
if (signal?.aborted !== true) yield {
|
|
848
913
|
type: "error",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rulvar/openai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.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.29.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.29.0"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|