@f5-sales-demo/pi-ai 22.7.0 → 22.7.2
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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/pi-ai",
|
|
4
|
-
"version": "22.7.
|
|
4
|
+
"version": "22.7.2",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@anthropic-ai/sdk": "0.124.0",
|
|
47
47
|
"@aws-sdk/client-bedrock-runtime": "3.1127.0",
|
|
48
48
|
"@bufbuild/protobuf": "2.14.1",
|
|
49
|
-
"@f5-sales-demo/pi-utils": "22.7.
|
|
49
|
+
"@f5-sales-demo/pi-utils": "22.7.2",
|
|
50
50
|
"@google/genai": "2.21.0",
|
|
51
51
|
"@sinclair/typebox": "0.34.52",
|
|
52
52
|
"@smithy/node-http-handler": "4.12.1",
|
|
@@ -192,7 +192,7 @@ function getCacheControl(
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
// Stealth mode: Mimic Claude Code headers and tool prefixing.
|
|
195
|
-
export const claudeCodeVersion = "2.1.
|
|
195
|
+
export const claudeCodeVersion = "2.1.287";
|
|
196
196
|
export const claudeToolPrefix: string = "proxy_";
|
|
197
197
|
export const claudeCodeSystemInstruction = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
|
198
198
|
|
|
@@ -26,6 +26,7 @@ import { getEnvApiKey } from "../stream";
|
|
|
26
26
|
import {
|
|
27
27
|
type Api,
|
|
28
28
|
type AssistantMessage,
|
|
29
|
+
type AssistantMessageEvent,
|
|
29
30
|
type Context,
|
|
30
31
|
isSpecialServiceTier,
|
|
31
32
|
type Model,
|
|
@@ -47,7 +48,7 @@ import {
|
|
|
47
48
|
} from "../utils";
|
|
48
49
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
49
50
|
import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
|
|
50
|
-
import { getOpenAIStreamIdleTimeoutMs
|
|
51
|
+
import { getOpenAIStreamIdleTimeoutMs } from "../utils/idle-iterator";
|
|
51
52
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
52
53
|
import { isTransportClosureError } from "../utils/retry";
|
|
53
54
|
import { adaptSchemaForStrict, NO_STRICT } from "../utils/schema";
|
|
@@ -185,7 +186,6 @@ interface CodexRequestContext {
|
|
|
185
186
|
|
|
186
187
|
interface CodexRequestSetup {
|
|
187
188
|
requestSignal: AbortSignal;
|
|
188
|
-
wrapCodexSseStream: (source: AsyncGenerator<Record<string, unknown>>) => AsyncGenerator<Record<string, unknown>>;
|
|
189
189
|
requestAbortController: AbortController;
|
|
190
190
|
}
|
|
191
191
|
|
|
@@ -195,6 +195,8 @@ interface CodexWebSocketRequestState {
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
interface CodexStreamRuntime {
|
|
198
|
+
stagedEvents: AssistantMessageEvent[];
|
|
199
|
+
firstTransportFailure?: unknown;
|
|
198
200
|
websocketRequestState?: CodexWebSocketRequestState;
|
|
199
201
|
continuationRecoveryAttempt: number;
|
|
200
202
|
sawResponseAccepted: boolean;
|
|
@@ -208,6 +210,7 @@ interface CodexStreamRuntime {
|
|
|
208
210
|
outputSlots: Map<number, { item: CodexEventItem | null; block: CodexOutputBlock | null; index: number }>;
|
|
209
211
|
currentIndex: number;
|
|
210
212
|
completedItemIds: Set<string>;
|
|
213
|
+
completedContentIndices: Set<number>;
|
|
211
214
|
currentBlock: CodexOutputBlock | null;
|
|
212
215
|
nativeOutputItems: Array<Record<string, unknown>>;
|
|
213
216
|
websocketStreamRetries: number;
|
|
@@ -219,6 +222,7 @@ interface CodexStreamRuntime {
|
|
|
219
222
|
}
|
|
220
223
|
|
|
221
224
|
interface CodexStreamProcessingContext {
|
|
225
|
+
runtime?: CodexStreamRuntime;
|
|
222
226
|
model: Model<"openai-codex-responses">;
|
|
223
227
|
output: AssistantMessage;
|
|
224
228
|
stream: AssistantMessageEventStream;
|
|
@@ -298,7 +302,13 @@ function getCodexProviderSessionState(
|
|
|
298
302
|
}
|
|
299
303
|
|
|
300
304
|
function createCodexWebSocketTransportError(message: string): Error {
|
|
301
|
-
return new Error(`${CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX}: ${message}`)
|
|
305
|
+
return Object.assign(new Error(`${CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX}: ${message}`), {
|
|
306
|
+
codexTransportInterruption: message.startsWith("websocket closed")
|
|
307
|
+
? "socket_closed"
|
|
308
|
+
: message === "idle timeout waiting for websocket" || message === "timeout waiting for first websocket event"
|
|
309
|
+
? "idle_timeout"
|
|
310
|
+
: undefined,
|
|
311
|
+
});
|
|
302
312
|
}
|
|
303
313
|
|
|
304
314
|
function isCodexWebSocketFatalError(error: Error): boolean {
|
|
@@ -453,15 +463,7 @@ function createRequestSetup(options: OpenAICodexResponsesOptions | undefined): C
|
|
|
453
463
|
const requestSignal = options?.signal
|
|
454
464
|
? AbortSignal.any([options.signal, requestAbortController.signal])
|
|
455
465
|
: requestAbortController.signal;
|
|
456
|
-
|
|
457
|
-
source: AsyncGenerator<Record<string, unknown>>,
|
|
458
|
-
): AsyncGenerator<Record<string, unknown>> =>
|
|
459
|
-
iterateWithIdleTimeout(source, {
|
|
460
|
-
idleTimeoutMs: getOpenAIStreamIdleTimeoutMs(),
|
|
461
|
-
errorMessage: "OpenAI Codex SSE stream stalled while waiting for the next event",
|
|
462
|
-
onIdle: () => requestAbortController.abort(),
|
|
463
|
-
});
|
|
464
|
-
return { requestAbortController, requestSignal, wrapCodexSseStream };
|
|
466
|
+
return { requestAbortController, requestSignal };
|
|
465
467
|
}
|
|
466
468
|
|
|
467
469
|
async function buildCodexRequestContext(
|
|
@@ -766,6 +768,60 @@ async function openCodexWebSocketTransport(
|
|
|
766
768
|
return { eventStream, requestBodyForState, transport: "websocket", websocketRequestState };
|
|
767
769
|
}
|
|
768
770
|
|
|
771
|
+
class CodexSseIdleTimeoutError extends Error {
|
|
772
|
+
constructor() {
|
|
773
|
+
super("OpenAI Codex SSE stream stalled while waiting for the next event");
|
|
774
|
+
this.name = "CodexSseIdleTimeoutError";
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
async function* iterateCodexSseAttempt(
|
|
779
|
+
source: AsyncGenerator<Record<string, unknown>>,
|
|
780
|
+
attempt: AbortController,
|
|
781
|
+
cleanup: () => void,
|
|
782
|
+
): AsyncGenerator<Record<string, unknown>> {
|
|
783
|
+
const timeoutMs = getOpenAIStreamIdleTimeoutMs();
|
|
784
|
+
try {
|
|
785
|
+
while (true) {
|
|
786
|
+
attempt.signal.throwIfAborted();
|
|
787
|
+
let timer: NodeJS.Timeout | undefined;
|
|
788
|
+
try {
|
|
789
|
+
const next = source.next();
|
|
790
|
+
const result =
|
|
791
|
+
timeoutMs === undefined
|
|
792
|
+
? await next
|
|
793
|
+
: await Promise.race([
|
|
794
|
+
next,
|
|
795
|
+
new Promise<never>((_resolve, reject) => {
|
|
796
|
+
timer = setTimeout(() => {
|
|
797
|
+
const error = new CodexSseIdleTimeoutError();
|
|
798
|
+
reject(error);
|
|
799
|
+
attempt.abort(error);
|
|
800
|
+
}, timeoutMs);
|
|
801
|
+
}),
|
|
802
|
+
]);
|
|
803
|
+
if (timer !== undefined) {
|
|
804
|
+
clearTimeout(timer);
|
|
805
|
+
timer = undefined;
|
|
806
|
+
}
|
|
807
|
+
attempt.signal.throwIfAborted();
|
|
808
|
+
if (result.done) return;
|
|
809
|
+
yield result.value;
|
|
810
|
+
} finally {
|
|
811
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
} finally {
|
|
815
|
+
attempt.abort();
|
|
816
|
+
cleanup();
|
|
817
|
+
try {
|
|
818
|
+
await source.return(undefined);
|
|
819
|
+
} catch {
|
|
820
|
+
/* Preserve the original stream failure. */
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
769
825
|
async function openCodexSseTransport(
|
|
770
826
|
requestContext: CodexRequestContext,
|
|
771
827
|
requestSetup: CodexRequestSetup,
|
|
@@ -779,8 +835,16 @@ async function openCodexSseTransport(
|
|
|
779
835
|
transport: CodexTransport;
|
|
780
836
|
websocketRequestState?: CodexWebSocketRequestState;
|
|
781
837
|
}> {
|
|
782
|
-
const
|
|
783
|
-
|
|
838
|
+
const attempt = new AbortController();
|
|
839
|
+
const signal = requestSetup.requestSignal;
|
|
840
|
+
const onAbort = () => attempt.abort(signal.reason);
|
|
841
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
842
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
843
|
+
if (signal.aborted) onAbort();
|
|
844
|
+
let source: AsyncGenerator<Record<string, unknown>>;
|
|
845
|
+
try {
|
|
846
|
+
attempt.signal.throwIfAborted();
|
|
847
|
+
source = await openCodexSseEventStream(
|
|
784
848
|
requestContext.url,
|
|
785
849
|
requestContext.requestHeaders,
|
|
786
850
|
requestContext.accountId,
|
|
@@ -788,10 +852,16 @@ async function openCodexSseTransport(
|
|
|
788
852
|
options?.sessionId,
|
|
789
853
|
body,
|
|
790
854
|
state,
|
|
791
|
-
|
|
855
|
+
attempt.signal,
|
|
792
856
|
retryFetch,
|
|
793
|
-
)
|
|
794
|
-
)
|
|
857
|
+
);
|
|
858
|
+
} catch (error) {
|
|
859
|
+
attempt.abort();
|
|
860
|
+
cleanup();
|
|
861
|
+
throw error;
|
|
862
|
+
}
|
|
863
|
+
const eventStream = iterateCodexSseAttempt(source, attempt, cleanup);
|
|
864
|
+
|
|
795
865
|
return { eventStream, requestBodyForState: structuredCloneJSON(body), transport: "sse" };
|
|
796
866
|
}
|
|
797
867
|
|
|
@@ -837,6 +907,7 @@ function createCodexStreamRuntime(initial: {
|
|
|
837
907
|
websocketRequestState?: CodexWebSocketRequestState;
|
|
838
908
|
}): CodexStreamRuntime {
|
|
839
909
|
return {
|
|
910
|
+
stagedEvents: [],
|
|
840
911
|
websocketRequestState: initial.websocketRequestState,
|
|
841
912
|
continuationRecoveryAttempt: 0,
|
|
842
913
|
sawResponseAccepted: false,
|
|
@@ -850,6 +921,7 @@ function createCodexStreamRuntime(initial: {
|
|
|
850
921
|
outputSlots: new Map(),
|
|
851
922
|
currentIndex: -1,
|
|
852
923
|
completedItemIds: new Set(),
|
|
924
|
+
completedContentIndices: new Set(),
|
|
853
925
|
nativeOutputItems: [],
|
|
854
926
|
websocketStreamRetries: 0,
|
|
855
927
|
providerRetryAttempt: 0,
|
|
@@ -860,6 +932,30 @@ function createCodexStreamRuntime(initial: {
|
|
|
860
932
|
};
|
|
861
933
|
}
|
|
862
934
|
|
|
935
|
+
// Empty normalized blocks belong to the attempt until replay becomes unsafe.
|
|
936
|
+
class CodexAttemptEventStream extends AssistantMessageEventStream {
|
|
937
|
+
constructor(
|
|
938
|
+
private readonly target: AssistantMessageEventStream,
|
|
939
|
+
private readonly runtime: CodexStreamRuntime,
|
|
940
|
+
) {
|
|
941
|
+
super();
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
override push(event: AssistantMessageEvent): void {
|
|
945
|
+
if (this.runtime.sawReplayUnsafeActivity || this.runtime.sawTerminalEvent) {
|
|
946
|
+
this.flush();
|
|
947
|
+
this.target.push(event);
|
|
948
|
+
} else {
|
|
949
|
+
this.runtime.stagedEvents.push(structuredCloneJSON(event));
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
flush(): void {
|
|
954
|
+
for (const event of this.runtime.stagedEvents) this.target.push(event);
|
|
955
|
+
this.runtime.stagedEvents.length = 0;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
863
959
|
async function processCodexResponseStream(
|
|
864
960
|
context: CodexStreamProcessingContext,
|
|
865
961
|
runtime: CodexStreamRuntime,
|
|
@@ -867,6 +963,7 @@ async function processCodexResponseStream(
|
|
|
867
963
|
const { output, stream } = context;
|
|
868
964
|
runtime.grammarToolInputProperties = context.requestContext.grammarToolInputProperties;
|
|
869
965
|
stream.push({ type: "start", partial: output });
|
|
966
|
+
const attemptStream = new CodexAttemptEventStream(stream, runtime);
|
|
870
967
|
|
|
871
968
|
while (true) {
|
|
872
969
|
try {
|
|
@@ -875,10 +972,12 @@ async function processCodexResponseStream(
|
|
|
875
972
|
await context.options?.onProviderStreamEvent?.(rawEvent, context.model);
|
|
876
973
|
firstTokenTime = handleCodexStreamEvent({
|
|
877
974
|
...context,
|
|
975
|
+
stream: attemptStream,
|
|
878
976
|
runtime,
|
|
879
977
|
rawEvent,
|
|
880
978
|
firstTokenTime,
|
|
881
979
|
});
|
|
980
|
+
if (runtime.sawReplayUnsafeActivity || runtime.sawTerminalEvent) attemptStream.flush();
|
|
882
981
|
}
|
|
883
982
|
if (!runtime.sawTerminalEvent) {
|
|
884
983
|
throw Object.assign(new Error("Codex stream ended before terminal completion event"), {
|
|
@@ -1047,6 +1146,7 @@ function handleCodexStreamEvent(args: {
|
|
|
1047
1146
|
if (itemId && runtime.completedItemIds.has(itemId)) return firstTokenTime;
|
|
1048
1147
|
if (itemId) runtime.completedItemIds.add(itemId);
|
|
1049
1148
|
handleOutputItemDone(model, output, stream, runtime, rawEvent, blockIndex);
|
|
1149
|
+
if (runtime.currentIndex >= 0) runtime.completedContentIndices.add(runtime.currentIndex);
|
|
1050
1150
|
runtime.outputSlots.delete(
|
|
1051
1151
|
typeof rawEvent.output_index === "number" ? rawEvent.output_index : runtime.currentIndex,
|
|
1052
1152
|
);
|
|
@@ -1215,6 +1315,7 @@ function handleOutputItemDone(
|
|
|
1215
1315
|
): void {
|
|
1216
1316
|
const item = structuredCloneJSON(rawEvent.item) as CodexEventItem;
|
|
1217
1317
|
runtime.nativeOutputItems.push(item as unknown as Record<string, unknown>);
|
|
1318
|
+
output.providerPayload = createOpenAIResponsesHistoryPayload(model.provider, runtime.nativeOutputItems);
|
|
1218
1319
|
if (item.type === "custom_tool_call" && runtime.currentBlock?.type === "toolCall") {
|
|
1219
1320
|
const property = runtime.grammarToolInputProperties?.get(item.name) ?? "input";
|
|
1220
1321
|
Object.assign(runtime.currentBlock, {
|
|
@@ -1270,7 +1371,7 @@ function handleOutputItemDone(
|
|
|
1270
1371
|
type: "toolCall",
|
|
1271
1372
|
id: `${item.call_id}|${item.id}`,
|
|
1272
1373
|
name: item.name,
|
|
1273
|
-
arguments:
|
|
1374
|
+
arguments: JSON.parse(item.arguments || "{}"),
|
|
1274
1375
|
};
|
|
1275
1376
|
if (runtime.currentBlock?.type === "toolCall") {
|
|
1276
1377
|
Object.assign(runtime.currentBlock, toolCall);
|
|
@@ -1409,7 +1510,7 @@ async function recoverCodexStreamError(
|
|
|
1409
1510
|
runtime: CodexStreamRuntime,
|
|
1410
1511
|
error: unknown,
|
|
1411
1512
|
): Promise<boolean> {
|
|
1412
|
-
if (runtime.transport === "sse" && isTransportClosureError(error)) {
|
|
1513
|
+
if (runtime.transport === "sse" && (error instanceof CodexSseIdleTimeoutError || isTransportClosureError(error))) {
|
|
1413
1514
|
return tryRestartCodexSseTransport(context, runtime, error);
|
|
1414
1515
|
}
|
|
1415
1516
|
if (isCodexRejectedContinuation(error)) {
|
|
@@ -1427,11 +1528,27 @@ async function recoverCodexStreamError(
|
|
|
1427
1528
|
return false;
|
|
1428
1529
|
}
|
|
1429
1530
|
|
|
1531
|
+
async function sleepBeforeCodexSseRestart(delayMs: number, signal: AbortSignal): Promise<void> {
|
|
1532
|
+
signal.throwIfAborted();
|
|
1533
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
1534
|
+
const timer = setTimeout(resolve, delayMs);
|
|
1535
|
+
const onAbort = () => reject(signal.reason);
|
|
1536
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1537
|
+
try {
|
|
1538
|
+
await promise;
|
|
1539
|
+
signal.throwIfAborted();
|
|
1540
|
+
} finally {
|
|
1541
|
+
clearTimeout(timer);
|
|
1542
|
+
signal.removeEventListener("abort", onAbort);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1430
1546
|
async function tryRestartCodexSseTransport(
|
|
1431
1547
|
context: CodexStreamProcessingContext,
|
|
1432
1548
|
runtime: CodexStreamRuntime,
|
|
1433
1549
|
originalError: unknown,
|
|
1434
1550
|
): Promise<boolean> {
|
|
1551
|
+
runtime.firstTransportFailure ??= originalError;
|
|
1435
1552
|
let error = originalError;
|
|
1436
1553
|
while (true) {
|
|
1437
1554
|
const replayEligible =
|
|
@@ -1451,22 +1568,29 @@ async function tryRestartCodexSseTransport(
|
|
|
1451
1568
|
terminalEventSeen: runtime.sawTerminalEvent,
|
|
1452
1569
|
replayEligible,
|
|
1453
1570
|
});
|
|
1454
|
-
if (!replayEligible || !isTransportClosureError(error)) {
|
|
1571
|
+
if (!replayEligible || !(error instanceof CodexSseIdleTimeoutError || isTransportClosureError(error))) {
|
|
1455
1572
|
if (error !== originalError) throw error;
|
|
1456
1573
|
return false;
|
|
1457
1574
|
}
|
|
1458
|
-
if (runtime.transportRestartAttempt >= 2)
|
|
1575
|
+
if (runtime.transportRestartAttempt >= Math.max(0, context.options?.maxRetries ?? 2))
|
|
1576
|
+
throw runtime.firstTransportFailure;
|
|
1459
1577
|
runtime.transportRestartAttempt += 1;
|
|
1460
|
-
|
|
1578
|
+
try {
|
|
1579
|
+
await runtime.eventStream.return(undefined);
|
|
1580
|
+
} catch {
|
|
1581
|
+
/* Preserve the recovery failure. */
|
|
1582
|
+
}
|
|
1461
1583
|
runtime.currentItem = null;
|
|
1462
1584
|
runtime.currentBlock = null;
|
|
1463
1585
|
runtime.currentIndex = -1;
|
|
1464
1586
|
runtime.outputSlots.clear();
|
|
1465
1587
|
runtime.completedItemIds.clear();
|
|
1588
|
+
runtime.completedContentIndices.clear();
|
|
1466
1589
|
runtime.nativeOutputItems.length = 0;
|
|
1467
1590
|
runtime.sawResponseAccepted = false;
|
|
1468
1591
|
runtime.sawOutputEvent = false;
|
|
1469
1592
|
runtime.sawTerminalEvent = false;
|
|
1593
|
+
runtime.stagedEvents.length = 0;
|
|
1470
1594
|
resetOutputState(context.output);
|
|
1471
1595
|
delete context.output.responseId;
|
|
1472
1596
|
delete context.output.providerPayload;
|
|
@@ -1480,7 +1604,10 @@ async function tryRestartCodexSseTransport(
|
|
|
1480
1604
|
resetCodexWebSocketAppendState(state);
|
|
1481
1605
|
resetCodexSessionMetadata(state);
|
|
1482
1606
|
}
|
|
1483
|
-
await
|
|
1607
|
+
await sleepBeforeCodexSseRestart(
|
|
1608
|
+
500 * (runtime.transportRestartAttempt + Math.max(0, 2 - (context.options?.maxRetries ?? 2))),
|
|
1609
|
+
context.requestSetup.requestSignal,
|
|
1610
|
+
);
|
|
1484
1611
|
try {
|
|
1485
1612
|
const next = await openCodexSseTransport(
|
|
1486
1613
|
context.requestContext,
|
|
@@ -1529,6 +1656,14 @@ async function tryRecoverCodexContinuation(
|
|
|
1529
1656
|
resetCodexWebSocketAppendState(state);
|
|
1530
1657
|
resetCodexSessionMetadata(state);
|
|
1531
1658
|
if (!eligible) return false;
|
|
1659
|
+
if (context.options?.maxRetries !== undefined) {
|
|
1660
|
+
if (runtime.transportRestartAttempt >= context.options.maxRetries) return false;
|
|
1661
|
+
runtime.transportRestartAttempt += 1;
|
|
1662
|
+
await sleepBeforeCodexSseRestart(
|
|
1663
|
+
500 * (runtime.transportRestartAttempt + Math.max(0, 2 - (context.options?.maxRetries ?? 2))),
|
|
1664
|
+
context.requestSetup.requestSignal,
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1532
1667
|
runtime.continuationRecoveryAttempt += 1;
|
|
1533
1668
|
logCodexDebug("codex continuation recovery", {
|
|
1534
1669
|
reason: "previous_response_unavailable",
|
|
@@ -1570,6 +1705,14 @@ async function tryReconnectCodexWebSocketOnConnectionLimit(
|
|
|
1570
1705
|
}
|
|
1571
1706
|
|
|
1572
1707
|
// Close the stale connection so getOrCreateCodexWebSocketConnection creates a fresh one.
|
|
1708
|
+
if (context.options?.maxRetries !== undefined) {
|
|
1709
|
+
if (runtime.transportRestartAttempt >= context.options.maxRetries) return false;
|
|
1710
|
+
runtime.transportRestartAttempt += 1;
|
|
1711
|
+
await sleepBeforeCodexSseRestart(
|
|
1712
|
+
500 * (runtime.transportRestartAttempt + Math.max(0, 2 - (context.options?.maxRetries ?? 2))),
|
|
1713
|
+
context.requestSetup.requestSignal,
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1573
1716
|
websocketState.connection?.close("connection_limit");
|
|
1574
1717
|
websocketState.connection = undefined;
|
|
1575
1718
|
resetCodexWebSocketAppendState(websocketState);
|
|
@@ -1588,7 +1731,9 @@ async function tryReconnectCodexWebSocketOnConnectionLimit(
|
|
|
1588
1731
|
runtime.currentBlock = null;
|
|
1589
1732
|
runtime.outputSlots.clear();
|
|
1590
1733
|
runtime.completedItemIds.clear();
|
|
1734
|
+
runtime.completedContentIndices.clear();
|
|
1591
1735
|
runtime.nativeOutputItems.length = 0;
|
|
1736
|
+
runtime.stagedEvents.length = 0;
|
|
1592
1737
|
resetOutputState(context.output);
|
|
1593
1738
|
context.firstTokenTime = undefined;
|
|
1594
1739
|
recordCodexWebSocketFailure(websocketState, true);
|
|
@@ -1616,6 +1761,14 @@ async function tryReplayWebsocketFailureOverSse(
|
|
|
1616
1761
|
!runtime.sawTerminalEvent &&
|
|
1617
1762
|
!context.options?.signal?.aborted;
|
|
1618
1763
|
if (!canReplay) return false;
|
|
1764
|
+
if (context.options?.maxRetries !== undefined) {
|
|
1765
|
+
if (runtime.transportRestartAttempt >= context.options.maxRetries) return false;
|
|
1766
|
+
runtime.transportRestartAttempt += 1;
|
|
1767
|
+
await sleepBeforeCodexSseRestart(
|
|
1768
|
+
500 * (runtime.transportRestartAttempt + Math.max(0, 2 - (context.options?.maxRetries ?? 2))),
|
|
1769
|
+
context.requestSetup.requestSignal,
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1619
1772
|
|
|
1620
1773
|
const state = websocketState;
|
|
1621
1774
|
const streamError = error instanceof Error ? error : new Error(String(error));
|
|
@@ -1649,7 +1802,9 @@ async function tryReplayWebsocketFailureOverSse(
|
|
|
1649
1802
|
runtime.currentBlock = null;
|
|
1650
1803
|
runtime.outputSlots.clear();
|
|
1651
1804
|
runtime.completedItemIds.clear();
|
|
1805
|
+
runtime.completedContentIndices.clear();
|
|
1652
1806
|
runtime.nativeOutputItems.length = 0;
|
|
1807
|
+
runtime.stagedEvents.length = 0;
|
|
1653
1808
|
resetOutputState(context.output);
|
|
1654
1809
|
context.firstTokenTime = undefined;
|
|
1655
1810
|
}
|
|
@@ -1690,7 +1845,9 @@ async function tryRetryCodexProviderError(
|
|
|
1690
1845
|
runtime.currentBlock = null;
|
|
1691
1846
|
runtime.outputSlots.clear();
|
|
1692
1847
|
runtime.completedItemIds.clear();
|
|
1848
|
+
runtime.completedContentIndices.clear();
|
|
1693
1849
|
runtime.sawTerminalEvent = false;
|
|
1850
|
+
runtime.stagedEvents.length = 0;
|
|
1694
1851
|
resetOutputState(context.output);
|
|
1695
1852
|
context.firstTokenTime = undefined;
|
|
1696
1853
|
await abortableSleep(CODEX_RETRY_DELAY_MS * runtime.providerRetryAttempt, context.requestSetup.requestSignal);
|
|
@@ -1752,6 +1909,37 @@ async function handleCodexStreamFailure(
|
|
|
1752
1909
|
resetCodexWebSocketAppendState(context.requestContext.websocketState);
|
|
1753
1910
|
resetCodexSessionMetadata(context.requestContext.websocketState);
|
|
1754
1911
|
}
|
|
1912
|
+
const runtime = context.runtime;
|
|
1913
|
+
const interrupted =
|
|
1914
|
+
!context.requestSetup.requestSignal.aborted &&
|
|
1915
|
+
runtime &&
|
|
1916
|
+
!runtime.sawTerminalEvent &&
|
|
1917
|
+
!(error instanceof CodexProviderStreamError) &&
|
|
1918
|
+
(error instanceof CodexSseIdleTimeoutError ||
|
|
1919
|
+
isTransportClosureError(error) ||
|
|
1920
|
+
(error as { codexTransportInterruption?: string })?.codexTransportInterruption);
|
|
1921
|
+
if (interrupted) {
|
|
1922
|
+
output.interruption = {
|
|
1923
|
+
transport: runtime.transport,
|
|
1924
|
+
classification:
|
|
1925
|
+
error instanceof CodexSseIdleTimeoutError ||
|
|
1926
|
+
(error as { codexTransportInterruption?: string }).codexTransportInterruption === "idle_timeout"
|
|
1927
|
+
? "idle_timeout"
|
|
1928
|
+
: (error as { code?: string }).code === "ERR_STREAM_PREMATURE_CLOSE"
|
|
1929
|
+
? "premature_eof"
|
|
1930
|
+
: "socket_closed",
|
|
1931
|
+
providerRetriesConsumed: runtime.transportRestartAttempt,
|
|
1932
|
+
completedContentIndices: [...runtime.completedContentIndices].sort((a, b) => a - b),
|
|
1933
|
+
};
|
|
1934
|
+
output.providerPayload = createOpenAIResponsesHistoryPayload(context.model.provider, runtime.nativeOutputItems);
|
|
1935
|
+
}
|
|
1936
|
+
if (runtime) {
|
|
1937
|
+
try {
|
|
1938
|
+
await runtime.eventStream.return(undefined);
|
|
1939
|
+
} catch {
|
|
1940
|
+
/* Preserve the original failure. */
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1755
1943
|
output.stopReason = context.options?.signal?.aborted ? "aborted" : "error";
|
|
1756
1944
|
output.errorMessage = await finalizeErrorMessage(error, context.requestContext.rawRequestDump);
|
|
1757
1945
|
output.providerFailureCode = sanitizeProviderFailureCode(
|
|
@@ -1791,6 +1979,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
|
|
|
1791
1979
|
}
|
|
1792
1980
|
|
|
1793
1981
|
processingContext = {
|
|
1982
|
+
runtime,
|
|
1794
1983
|
model,
|
|
1795
1984
|
output,
|
|
1796
1985
|
stream,
|
|
@@ -2465,7 +2654,39 @@ async function openCodexSseEventStream(
|
|
|
2465
2654
|
if (!response.body) {
|
|
2466
2655
|
throw new Error("No response body");
|
|
2467
2656
|
}
|
|
2468
|
-
|
|
2657
|
+
signal?.throwIfAborted();
|
|
2658
|
+
const reader = response.body.getReader();
|
|
2659
|
+
let closed = false;
|
|
2660
|
+
const close = () => {
|
|
2661
|
+
if (closed) return;
|
|
2662
|
+
closed = true;
|
|
2663
|
+
// Cancel settles pending reads synchronously; do not await an upstream cancel hook.
|
|
2664
|
+
void reader.cancel().catch(() => {});
|
|
2665
|
+
reader.releaseLock();
|
|
2666
|
+
};
|
|
2667
|
+
const onAbort = () => close();
|
|
2668
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2669
|
+
const owned = new ReadableStream<Uint8Array>({
|
|
2670
|
+
async pull(controller) {
|
|
2671
|
+
try {
|
|
2672
|
+
const result = await reader.read();
|
|
2673
|
+
if (result.done) controller.close();
|
|
2674
|
+
else controller.enqueue(result.value);
|
|
2675
|
+
} catch (error) {
|
|
2676
|
+
controller.error(error);
|
|
2677
|
+
}
|
|
2678
|
+
},
|
|
2679
|
+
cancel: close,
|
|
2680
|
+
});
|
|
2681
|
+
async function* events(): AsyncGenerator<Record<string, unknown>> {
|
|
2682
|
+
try {
|
|
2683
|
+
yield* readSseJson<Record<string, unknown>>(owned);
|
|
2684
|
+
} finally {
|
|
2685
|
+
close();
|
|
2686
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
return events();
|
|
2469
2690
|
}
|
|
2470
2691
|
|
|
2471
2692
|
function createCodexHeaders(
|
|
@@ -2554,6 +2775,7 @@ async function fetchWithRetry(url: string, init: RequestInit, signal?: AbortSign
|
|
|
2554
2775
|
let rateLimitTimeSpent = 0;
|
|
2555
2776
|
while (true) {
|
|
2556
2777
|
try {
|
|
2778
|
+
signal?.throwIfAborted();
|
|
2557
2779
|
const response = await fetch(url, { ...init, signal: signal ?? init.signal });
|
|
2558
2780
|
if (!retry || !CODEX_RETRYABLE_STATUS.has(response.status)) {
|
|
2559
2781
|
return response;
|
|
@@ -2681,7 +2903,7 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
|
|
2681
2903
|
|
|
2682
2904
|
const outputItems: ResponseInput = [];
|
|
2683
2905
|
for (const block of msg.content) {
|
|
2684
|
-
if (block.type === "thinking" && msg.stopReason !== "error") {
|
|
2906
|
+
if (block.type === "thinking" && (msg.stopReason !== "error" || !!assistantMsg.interruption)) {
|
|
2685
2907
|
if (block.thinkingSignature) {
|
|
2686
2908
|
outputItems.push(JSON.parse(block.thinkingSignature) as ResponseReasoningItem);
|
|
2687
2909
|
}
|
|
@@ -135,7 +135,7 @@ export function convertResponsesAssistantMessage<TApi extends Api>(
|
|
|
135
135
|
assistantMsg.model !== model.id && assistantMsg.provider === model.provider && assistantMsg.api === model.api;
|
|
136
136
|
|
|
137
137
|
for (const block of assistantMsg.content) {
|
|
138
|
-
if (block.type === "thinking" && assistantMsg.stopReason !== "error") {
|
|
138
|
+
if (block.type === "thinking" && (assistantMsg.stopReason !== "error" || !!assistantMsg.interruption)) {
|
|
139
139
|
if (!includeThinkingSignatures) {
|
|
140
140
|
continue;
|
|
141
141
|
}
|
|
@@ -69,7 +69,9 @@ export function transformMessages<TApi extends Api>(
|
|
|
69
69
|
// Aborted/errored messages may have partially-streamed thinking signatures.
|
|
70
70
|
// A partial signature is invalid and will be rejected by the API, so we must
|
|
71
71
|
// strip signatures from thinking blocks in these messages.
|
|
72
|
-
const hasInvalidSignatures =
|
|
72
|
+
const hasInvalidSignatures =
|
|
73
|
+
assistantMsg.stopReason === "aborted" ||
|
|
74
|
+
(assistantMsg.stopReason === "error" && !assistantMsg.interruption);
|
|
73
75
|
|
|
74
76
|
const transformedContent = assistantMsg.content.flatMap(block => {
|
|
75
77
|
if (block.type === "thinking") {
|
|
@@ -205,7 +207,10 @@ export function transformMessages<TApi extends Api>(
|
|
|
205
207
|
const assistantMsg = msg as AssistantMessage;
|
|
206
208
|
const toolCalls = assistantMsg.content.filter(b => b.type === "toolCall") as ToolCall[];
|
|
207
209
|
|
|
208
|
-
if (
|
|
210
|
+
if (
|
|
211
|
+
(assistantMsg.stopReason === "error" && !assistantMsg.interruption) ||
|
|
212
|
+
assistantMsg.stopReason === "aborted"
|
|
213
|
+
) {
|
|
209
214
|
// Keep the assistant message with tool calls intact. If real tool results follow, preserve them;
|
|
210
215
|
// otherwise synthesize aborted results before the next turn boundary.
|
|
211
216
|
result.push(msg);
|
package/src/types.ts
CHANGED
|
@@ -442,6 +442,14 @@ export interface DeveloperMessage {
|
|
|
442
442
|
}
|
|
443
443
|
|
|
444
444
|
export interface AssistantMessage {
|
|
445
|
+
/** Completed content retained after a typed transport interruption. */
|
|
446
|
+
interruption?: {
|
|
447
|
+
transport: "sse" | "websocket";
|
|
448
|
+
classification: "socket_closed" | "premature_eof" | "idle_timeout";
|
|
449
|
+
providerRetriesConsumed: number;
|
|
450
|
+
toolChoiceServed?: boolean;
|
|
451
|
+
completedContentIndices: number[];
|
|
452
|
+
};
|
|
445
453
|
/** A queued background request; poll using the same provider route. */
|
|
446
454
|
deferred?: DeferredHandle;
|
|
447
455
|
providerRewrite?: {
|