@howaboua/pi-codex-conversion 1.0.24 → 1.0.26
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
|
@@ -105,6 +105,12 @@ interface SessionWebSocketCacheEntry {
|
|
|
105
105
|
idleTimer?: ReturnType<typeof setTimeout>;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
interface AcquiredWebSocket {
|
|
109
|
+
socket: WebSocketLike;
|
|
110
|
+
reused: boolean;
|
|
111
|
+
release: (options?: { keep?: boolean }) => void;
|
|
112
|
+
}
|
|
113
|
+
|
|
108
114
|
let fsPromisesPromise: Promise<typeof import("node:fs/promises")> | undefined;
|
|
109
115
|
const workspaceRootCache = new Map<string, Promise<string>>();
|
|
110
116
|
|
|
@@ -753,12 +759,13 @@ async function acquireWebSocket(
|
|
|
753
759
|
headers: Headers,
|
|
754
760
|
sessionId: string | undefined,
|
|
755
761
|
signal: AbortSignal | undefined,
|
|
756
|
-
): Promise<
|
|
762
|
+
): Promise<AcquiredWebSocket> {
|
|
757
763
|
const cacheKey = buildWebSocketCacheKey(url, headers, sessionId);
|
|
758
764
|
if (!cacheKey) {
|
|
759
765
|
const socket = await connectWebSocket(url, headers, signal);
|
|
760
766
|
return {
|
|
761
767
|
socket,
|
|
768
|
+
reused: false,
|
|
762
769
|
release: ({ keep } = {}) => {
|
|
763
770
|
if (keep === false) {
|
|
764
771
|
closeWebSocketSilently(socket);
|
|
@@ -780,6 +787,7 @@ async function acquireWebSocket(
|
|
|
780
787
|
cached.busy = true;
|
|
781
788
|
return {
|
|
782
789
|
socket: cached.socket,
|
|
790
|
+
reused: true,
|
|
783
791
|
release: ({ keep } = {}) => {
|
|
784
792
|
if (!keep || !isWebSocketReusable(cached.socket)) {
|
|
785
793
|
closeWebSocketSilently(cached.socket);
|
|
@@ -796,6 +804,7 @@ async function acquireWebSocket(
|
|
|
796
804
|
const socket = await connectWebSocket(url, headers, signal);
|
|
797
805
|
return {
|
|
798
806
|
socket,
|
|
807
|
+
reused: false,
|
|
799
808
|
release: () => {
|
|
800
809
|
closeWebSocketSilently(socket);
|
|
801
810
|
},
|
|
@@ -813,6 +822,7 @@ async function acquireWebSocket(
|
|
|
813
822
|
websocketSessionCache.set(cacheKey, entry);
|
|
814
823
|
return {
|
|
815
824
|
socket,
|
|
825
|
+
reused: false,
|
|
816
826
|
release: ({ keep } = {}) => {
|
|
817
827
|
if (!keep || !isWebSocketReusable(entry.socket)) {
|
|
818
828
|
closeWebSocketSilently(entry.socket);
|
|
@@ -948,6 +958,21 @@ async function* parseWebSocket(socket: WebSocketLike, signal: AbortSignal | unde
|
|
|
948
958
|
}
|
|
949
959
|
}
|
|
950
960
|
|
|
961
|
+
async function* countWebSocketEvents(
|
|
962
|
+
events: AsyncIterable<StreamEventShape>,
|
|
963
|
+
onEvent: () => void,
|
|
964
|
+
): AsyncIterable<StreamEventShape> {
|
|
965
|
+
for await (const event of events) {
|
|
966
|
+
onEvent();
|
|
967
|
+
yield event;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function isRetryableEarlyWebSocketError(error: unknown): boolean {
|
|
972
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
973
|
+
return /^WebSocket (error|closed)(?:\s|$)/.test(message);
|
|
974
|
+
}
|
|
975
|
+
|
|
951
976
|
async function* mapCodexEvents(events: AsyncIterable<StreamEventShape>): AsyncIterable<StreamEventShape> {
|
|
952
977
|
let sawTerminalResponse = false;
|
|
953
978
|
for await (const event of events) {
|
|
@@ -1100,22 +1125,58 @@ async function processWebSocketStream<TApi extends Api>(
|
|
|
1100
1125
|
cwd: string,
|
|
1101
1126
|
requestPrompt: string | undefined,
|
|
1102
1127
|
): Promise<void> {
|
|
1103
|
-
|
|
1104
|
-
|
|
1128
|
+
let streamStarted = false;
|
|
1129
|
+
|
|
1130
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1131
|
+
const { socket, release, reused } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
|
|
1132
|
+
let keepConnection = true;
|
|
1133
|
+
let released = false;
|
|
1134
|
+
let eventCount = 0;
|
|
1135
|
+
|
|
1136
|
+
const releaseOnce = (releaseOptions?: { keep?: boolean }) => {
|
|
1137
|
+
if (released) return;
|
|
1138
|
+
released = true;
|
|
1139
|
+
release(releaseOptions);
|
|
1140
|
+
};
|
|
1105
1141
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1142
|
+
try {
|
|
1143
|
+
socket.send(JSON.stringify({ type: "response.create", ...body }));
|
|
1144
|
+
if (!streamStarted) {
|
|
1145
|
+
onStart();
|
|
1146
|
+
stream.push({ type: "start", partial: output });
|
|
1147
|
+
streamStarted = true;
|
|
1148
|
+
}
|
|
1149
|
+
await processCapturedResponsesStream(
|
|
1150
|
+
countWebSocketEvents(parseWebSocket(socket, options?.signal), () => {
|
|
1151
|
+
eventCount++;
|
|
1152
|
+
}),
|
|
1153
|
+
output,
|
|
1154
|
+
stream,
|
|
1155
|
+
model,
|
|
1156
|
+
options,
|
|
1157
|
+
deps,
|
|
1158
|
+
cwd,
|
|
1159
|
+
requestPrompt,
|
|
1160
|
+
);
|
|
1161
|
+
if (options?.signal?.aborted) {
|
|
1162
|
+
keepConnection = false;
|
|
1163
|
+
}
|
|
1164
|
+
releaseOnce({ keep: keepConnection });
|
|
1165
|
+
return;
|
|
1166
|
+
} catch (error) {
|
|
1112
1167
|
keepConnection = false;
|
|
1168
|
+
releaseOnce({ keep: false });
|
|
1169
|
+
// Pi's stock provider reuses session WebSockets. In practice the Codex
|
|
1170
|
+
// backend sometimes cleanly closes an idle cached socket between turns;
|
|
1171
|
+
// if that stale socket fails before any response event, retry once on a
|
|
1172
|
+
// fresh WebSocket without changing request shape or falling back transports.
|
|
1173
|
+
if (attempt === 0 && reused && eventCount === 0 && !options?.signal?.aborted && isRetryableEarlyWebSocketError(error)) {
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
throw error;
|
|
1177
|
+
} finally {
|
|
1178
|
+
releaseOnce({ keep: keepConnection });
|
|
1113
1179
|
}
|
|
1114
|
-
} catch (error) {
|
|
1115
|
-
keepConnection = false;
|
|
1116
|
-
throw error;
|
|
1117
|
-
} finally {
|
|
1118
|
-
release({ keep: keepConnection });
|
|
1119
1180
|
}
|
|
1120
1181
|
}
|
|
1121
1182
|
|
|
@@ -8,12 +8,10 @@ type Message = Context["messages"][number];
|
|
|
8
8
|
|
|
9
9
|
interface ImageGenerationCallItem {
|
|
10
10
|
type: "image_generation_call";
|
|
11
|
-
id
|
|
12
|
-
status
|
|
13
|
-
result
|
|
14
|
-
output_format?: string;
|
|
11
|
+
id: string;
|
|
12
|
+
status: string;
|
|
13
|
+
result: string | null;
|
|
15
14
|
revised_prompt?: string;
|
|
16
|
-
[key: string]: unknown;
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
interface ImageGenerationCallBlock {
|
|
@@ -76,6 +74,23 @@ function isImageGenerationCallBlock(block: InternalAssistantContent): block is I
|
|
|
76
74
|
return block.type === "image_generation_call" && block.item?.type === "image_generation_call";
|
|
77
75
|
}
|
|
78
76
|
|
|
77
|
+
function sanitizeImageGenerationCallItem(item: unknown): ImageGenerationCallItem | undefined {
|
|
78
|
+
if (!item || typeof item !== "object") return undefined;
|
|
79
|
+
const candidate = item as Record<string, unknown>;
|
|
80
|
+
if (candidate.type !== "image_generation_call") return undefined;
|
|
81
|
+
if (typeof candidate.id !== "string" || candidate.id === "") return undefined;
|
|
82
|
+
if (typeof candidate.status !== "string" || candidate.status === "") return undefined;
|
|
83
|
+
if (!(typeof candidate.result === "string" || candidate.result === null)) return undefined;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
type: "image_generation_call",
|
|
87
|
+
id: candidate.id,
|
|
88
|
+
status: candidate.status,
|
|
89
|
+
result: candidate.result,
|
|
90
|
+
...(typeof candidate.revised_prompt === "string" ? { revised_prompt: candidate.revised_prompt } : {}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
79
94
|
const NON_VISION_USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)";
|
|
80
95
|
const NON_VISION_TOOL_IMAGE_PLACEHOLDER = "(tool image omitted: model does not support images)";
|
|
81
96
|
|
|
@@ -287,7 +302,8 @@ export function convertResponsesMessages<TApi extends Api>(
|
|
|
287
302
|
let assistantBlockIndex = 0;
|
|
288
303
|
for (const block of msg.content as InternalAssistantContent[]) {
|
|
289
304
|
if (isImageGenerationCallBlock(block)) {
|
|
290
|
-
|
|
305
|
+
const imageGenerationCall = sanitizeImageGenerationCallItem(block.item);
|
|
306
|
+
if (imageGenerationCall) output.push(imageGenerationCall as ResponseInput[number]);
|
|
291
307
|
} else if (block.type === "thinking") {
|
|
292
308
|
if (block.thinkingSignature) output.push(JSON.parse(block.thinkingSignature));
|
|
293
309
|
} else if (block.type === "text") {
|
|
@@ -581,10 +597,13 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
581
597
|
stream.push({ type: "toolcall_end", contentIndex: toolCallIndex, toolCall, partial: output });
|
|
582
598
|
outputStates.delete(event.output_index);
|
|
583
599
|
} else if (item.type === "image_generation_call") {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
600
|
+
const imageGenerationCall = sanitizeImageGenerationCallItem(item);
|
|
601
|
+
if (imageGenerationCall) {
|
|
602
|
+
(output.content as InternalAssistantContent[]).push({
|
|
603
|
+
type: "image_generation_call",
|
|
604
|
+
item: imageGenerationCall,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
588
607
|
outputStates.delete(event.output_index);
|
|
589
608
|
}
|
|
590
609
|
} else if (event.type === "response.completed") {
|