@howaboua/pi-codex-conversion 1.0.28 → 1.0.30
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/README.md +1 -1
- package/package.json +7 -7
- package/src/providers/openai-codex-custom-provider.ts +114 -29
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ This package replaces Pi's default Codex/GPT experience with a narrower Codex-li
|
|
|
9
9
|
- preserves Pi's composed system prompt and applies a narrow Codex-oriented delta on top
|
|
10
10
|
- renders exec activity with Codex-style command and background-terminal labels
|
|
11
11
|
- renders `apply_patch` calls with Codex-style `Added` / `Edited` / `Deleted` diff blocks and Pi-style colored diff lines
|
|
12
|
-
- targets modern Pi tool/rendering APIs and is aligned with Pi `0.
|
|
12
|
+
- targets modern Pi tool/rendering APIs and is aligned with Pi `0.72.x`
|
|
13
13
|
|
|
14
14
|

|
|
15
15
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@howaboua/pi-codex-conversion",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.30",
|
|
4
4
|
"description": "Codex-oriented tool and prompt adapter for pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -51,15 +51,15 @@
|
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@mariozechner/pi-ai": "^0.
|
|
55
|
-
"@mariozechner/pi-coding-agent": "^0.
|
|
56
|
-
"@mariozechner/pi-tui": "^0.
|
|
54
|
+
"@mariozechner/pi-ai": "^0.73.0",
|
|
55
|
+
"@mariozechner/pi-coding-agent": "^0.73.0",
|
|
56
|
+
"@mariozechner/pi-tui": "^0.73.0",
|
|
57
57
|
"typebox": "^1.1.24"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@mariozechner/pi-ai": "^0.
|
|
61
|
-
"@mariozechner/pi-coding-agent": "^0.
|
|
62
|
-
"@mariozechner/pi-tui": "^0.
|
|
60
|
+
"@mariozechner/pi-ai": "^0.73.0",
|
|
61
|
+
"@mariozechner/pi-coding-agent": "^0.73.0",
|
|
62
|
+
"@mariozechner/pi-tui": "^0.73.0",
|
|
63
63
|
"tsx": "^4.20.5",
|
|
64
64
|
"typebox": "^1.1.24",
|
|
65
65
|
"typescript": "^5.9.3"
|
|
@@ -2,8 +2,10 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
|
2
2
|
import { Box, Image, Spacer, Text } from "@mariozechner/pi-tui";
|
|
3
3
|
import {
|
|
4
4
|
createAssistantMessageEventStream,
|
|
5
|
+
appendAssistantMessageDiagnostic,
|
|
6
|
+
clampThinkingLevel,
|
|
7
|
+
createAssistantMessageDiagnostic,
|
|
5
8
|
getEnvApiKey,
|
|
6
|
-
supportsXhigh,
|
|
7
9
|
type Api,
|
|
8
10
|
type AssistantMessage,
|
|
9
11
|
type AssistantMessageEventStream,
|
|
@@ -103,14 +105,22 @@ interface SessionWebSocketCacheEntry {
|
|
|
103
105
|
socket: WebSocketLike;
|
|
104
106
|
busy: boolean;
|
|
105
107
|
idleTimer?: ReturnType<typeof setTimeout>;
|
|
108
|
+
continuation?: CachedWebSocketContinuationState;
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
interface AcquiredWebSocket {
|
|
109
112
|
socket: WebSocketLike;
|
|
113
|
+
entry?: SessionWebSocketCacheEntry;
|
|
110
114
|
reused: boolean;
|
|
111
115
|
release: (options?: { keep?: boolean }) => void;
|
|
112
116
|
}
|
|
113
117
|
|
|
118
|
+
interface CachedWebSocketContinuationState {
|
|
119
|
+
lastRequestBody: ResponsesBody;
|
|
120
|
+
lastResponseId: string;
|
|
121
|
+
lastResponseItems: unknown[];
|
|
122
|
+
}
|
|
123
|
+
|
|
114
124
|
let fsPromisesPromise: Promise<typeof import("node:fs/promises")> | undefined;
|
|
115
125
|
const workspaceRootCache = new Map<string, Promise<string>>();
|
|
116
126
|
|
|
@@ -121,7 +131,8 @@ interface ResponsesBody {
|
|
|
121
131
|
store: boolean;
|
|
122
132
|
stream: boolean;
|
|
123
133
|
instructions?: string;
|
|
124
|
-
|
|
134
|
+
previous_response_id?: string;
|
|
135
|
+
input: unknown[];
|
|
125
136
|
text: { verbosity: string };
|
|
126
137
|
include: string[];
|
|
127
138
|
prompt_cache_key?: string;
|
|
@@ -403,16 +414,6 @@ function headersToRecord(headers: Headers): Record<string, string> {
|
|
|
403
414
|
return Object.fromEntries(headers.entries());
|
|
404
415
|
}
|
|
405
416
|
|
|
406
|
-
function buildWebSocketCacheKey(url: string, headers: Headers, sessionId: string | undefined): string | undefined {
|
|
407
|
-
if (!sessionId) return undefined;
|
|
408
|
-
const headerFingerprint = Object.entries(headersToRecord(headers))
|
|
409
|
-
.map(([key, value]) => [key.toLowerCase(), value] as const)
|
|
410
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
411
|
-
.map(([key, value]) => `${key}:${value}`)
|
|
412
|
-
.join("\n");
|
|
413
|
-
return `${sessionId}:${shortHash(`${url}\n${headerFingerprint}`)}`;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
417
|
function createCodexRequestId(): string {
|
|
417
418
|
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
418
419
|
return globalThis.crypto.randomUUID();
|
|
@@ -554,10 +555,13 @@ function buildRequestBody<TApi extends Api>(model: Model<TApi>, context: Context
|
|
|
554
555
|
}
|
|
555
556
|
}
|
|
556
557
|
|
|
557
|
-
|
|
558
|
-
|
|
558
|
+
const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
|
|
559
|
+
const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
|
|
560
|
+
if (reasoningEffort !== undefined) {
|
|
561
|
+
const effort = model.thinkingLevelMap?.[reasoningEffort] ?? reasoningEffort;
|
|
562
|
+
if (effort === null) return body;
|
|
559
563
|
body.reasoning = {
|
|
560
|
-
effort: clampReasoningEffort(model.id,
|
|
564
|
+
effort: clampReasoningEffort(model.id, effort),
|
|
561
565
|
summary: ((options as { reasoningSummary?: string } | undefined)?.reasoningSummary ?? "auto") as string,
|
|
562
566
|
};
|
|
563
567
|
}
|
|
@@ -662,6 +666,7 @@ function closeWebSocketSilently(socket: WebSocketLike, code = 1000, reason = "do
|
|
|
662
666
|
}
|
|
663
667
|
}
|
|
664
668
|
|
|
669
|
+
|
|
665
670
|
function scheduleSessionWebSocketExpiry(cacheKey: string, entry: SessionWebSocketCacheEntry): void {
|
|
666
671
|
if (entry.idleTimer) {
|
|
667
672
|
clearTimeout(entry.idleTimer);
|
|
@@ -760,8 +765,7 @@ async function acquireWebSocket(
|
|
|
760
765
|
sessionId: string | undefined,
|
|
761
766
|
signal: AbortSignal | undefined,
|
|
762
767
|
): Promise<AcquiredWebSocket> {
|
|
763
|
-
|
|
764
|
-
if (!cacheKey) {
|
|
768
|
+
if (!sessionId) {
|
|
765
769
|
const socket = await connectWebSocket(url, headers, signal);
|
|
766
770
|
return {
|
|
767
771
|
socket,
|
|
@@ -776,7 +780,7 @@ async function acquireWebSocket(
|
|
|
776
780
|
};
|
|
777
781
|
}
|
|
778
782
|
|
|
779
|
-
const cached = websocketSessionCache.get(
|
|
783
|
+
const cached = websocketSessionCache.get(sessionId);
|
|
780
784
|
if (cached) {
|
|
781
785
|
if (cached.idleTimer) {
|
|
782
786
|
clearTimeout(cached.idleTimer);
|
|
@@ -787,15 +791,16 @@ async function acquireWebSocket(
|
|
|
787
791
|
cached.busy = true;
|
|
788
792
|
return {
|
|
789
793
|
socket: cached.socket,
|
|
794
|
+
entry: cached,
|
|
790
795
|
reused: true,
|
|
791
796
|
release: ({ keep } = {}) => {
|
|
792
797
|
if (!keep || !isWebSocketReusable(cached.socket)) {
|
|
793
798
|
closeWebSocketSilently(cached.socket);
|
|
794
|
-
websocketSessionCache.delete(
|
|
799
|
+
websocketSessionCache.delete(sessionId);
|
|
795
800
|
return;
|
|
796
801
|
}
|
|
797
802
|
cached.busy = false;
|
|
798
|
-
scheduleSessionWebSocketExpiry(
|
|
803
|
+
scheduleSessionWebSocketExpiry(sessionId, cached);
|
|
799
804
|
},
|
|
800
805
|
};
|
|
801
806
|
}
|
|
@@ -813,27 +818,28 @@ async function acquireWebSocket(
|
|
|
813
818
|
|
|
814
819
|
if (!isWebSocketReusable(cached.socket)) {
|
|
815
820
|
closeWebSocketSilently(cached.socket);
|
|
816
|
-
websocketSessionCache.delete(
|
|
821
|
+
websocketSessionCache.delete(sessionId);
|
|
817
822
|
}
|
|
818
823
|
}
|
|
819
824
|
|
|
820
825
|
const socket = await connectWebSocket(url, headers, signal);
|
|
821
826
|
const entry: SessionWebSocketCacheEntry = { socket, busy: true };
|
|
822
|
-
websocketSessionCache.set(
|
|
827
|
+
websocketSessionCache.set(sessionId, entry);
|
|
823
828
|
return {
|
|
824
829
|
socket,
|
|
830
|
+
entry,
|
|
825
831
|
reused: false,
|
|
826
832
|
release: ({ keep } = {}) => {
|
|
827
833
|
if (!keep || !isWebSocketReusable(entry.socket)) {
|
|
828
834
|
closeWebSocketSilently(entry.socket);
|
|
829
835
|
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
830
|
-
if (websocketSessionCache.get(
|
|
831
|
-
websocketSessionCache.delete(
|
|
836
|
+
if (websocketSessionCache.get(sessionId) === entry) {
|
|
837
|
+
websocketSessionCache.delete(sessionId);
|
|
832
838
|
}
|
|
833
839
|
return;
|
|
834
840
|
}
|
|
835
841
|
entry.busy = false;
|
|
836
|
-
scheduleSessionWebSocketExpiry(
|
|
842
|
+
scheduleSessionWebSocketExpiry(sessionId, entry);
|
|
837
843
|
},
|
|
838
844
|
};
|
|
839
845
|
}
|
|
@@ -853,6 +859,57 @@ async function decodeWebSocketData(data: unknown): Promise<string | null> {
|
|
|
853
859
|
return null;
|
|
854
860
|
}
|
|
855
861
|
|
|
862
|
+
function requestBodyWithoutInput(body: ResponsesBody): ResponsesBody {
|
|
863
|
+
const { input: _input, previous_response_id: _previousResponseId, ...rest } = body;
|
|
864
|
+
return rest as ResponsesBody;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function responseInputsEqual(a: unknown[] | undefined, b: unknown[] | undefined): boolean {
|
|
868
|
+
return JSON.stringify(a ?? []) === JSON.stringify(b ?? []);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function requestBodiesMatchExceptInput(a: ResponsesBody, b: ResponsesBody): boolean {
|
|
872
|
+
return JSON.stringify(requestBodyWithoutInput(a)) === JSON.stringify(requestBodyWithoutInput(b));
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function getCachedWebSocketInputDelta(body: ResponsesBody, continuation: CachedWebSocketContinuationState): unknown[] | undefined {
|
|
876
|
+
if (!requestBodiesMatchExceptInput(body, continuation.lastRequestBody)) {
|
|
877
|
+
return undefined;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const currentInput = body.input ?? [];
|
|
881
|
+
const baseline = [...(continuation.lastRequestBody.input ?? []), ...continuation.lastResponseItems];
|
|
882
|
+
if (currentInput.length < baseline.length) {
|
|
883
|
+
return undefined;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const prefix = currentInput.slice(0, baseline.length);
|
|
887
|
+
if (!responseInputsEqual(prefix, baseline)) {
|
|
888
|
+
return undefined;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return currentInput.slice(baseline.length);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function buildCachedWebSocketRequestBody(entry: SessionWebSocketCacheEntry, body: ResponsesBody): ResponsesBody {
|
|
895
|
+
const continuation = entry.continuation;
|
|
896
|
+
if (!continuation) {
|
|
897
|
+
return body;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const delta = getCachedWebSocketInputDelta(body, continuation);
|
|
901
|
+
if (!delta || !continuation.lastResponseId) {
|
|
902
|
+
entry.continuation = undefined;
|
|
903
|
+
return body;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
return {
|
|
907
|
+
...body,
|
|
908
|
+
previous_response_id: continuation.lastResponseId,
|
|
909
|
+
input: delta,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
856
913
|
async function* parseWebSocket(socket: WebSocketLike, signal: AbortSignal | undefined): AsyncIterable<StreamEventShape> {
|
|
857
914
|
const queue: StreamEventShape[] = [];
|
|
858
915
|
let pending: (() => void) | null = null;
|
|
@@ -1128,10 +1185,16 @@ async function processWebSocketStream<TApi extends Api>(
|
|
|
1128
1185
|
let streamStarted = false;
|
|
1129
1186
|
|
|
1130
1187
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1131
|
-
const { socket, release, reused } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
|
|
1188
|
+
const { socket, entry, release, reused } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
|
|
1132
1189
|
let keepConnection = true;
|
|
1133
1190
|
let released = false;
|
|
1134
1191
|
let eventCount = 0;
|
|
1192
|
+
const transport = (options as { transport?: string } | undefined)?.transport ?? "auto";
|
|
1193
|
+
const useCachedContext = transport === "websocket-cached" || transport === "auto";
|
|
1194
|
+
// ChatGPT Codex Responses rejects `store: true` ("Store must be set to false").
|
|
1195
|
+
// WebSocket continuation still works via connection-scoped previous_response_id state.
|
|
1196
|
+
const fullBody = body;
|
|
1197
|
+
const requestBody = useCachedContext && entry ? buildCachedWebSocketRequestBody(entry, fullBody) : fullBody;
|
|
1135
1198
|
|
|
1136
1199
|
const releaseOnce = (releaseOptions?: { keep?: boolean }) => {
|
|
1137
1200
|
if (released) return;
|
|
@@ -1140,7 +1203,7 @@ async function processWebSocketStream<TApi extends Api>(
|
|
|
1140
1203
|
};
|
|
1141
1204
|
|
|
1142
1205
|
try {
|
|
1143
|
-
socket.send(JSON.stringify({ type: "response.create", ...
|
|
1206
|
+
socket.send(JSON.stringify({ type: "response.create", ...requestBody }));
|
|
1144
1207
|
if (!streamStarted) {
|
|
1145
1208
|
onStart();
|
|
1146
1209
|
stream.push({ type: "start", partial: output });
|
|
@@ -1160,10 +1223,22 @@ async function processWebSocketStream<TApi extends Api>(
|
|
|
1160
1223
|
);
|
|
1161
1224
|
if (options?.signal?.aborted) {
|
|
1162
1225
|
keepConnection = false;
|
|
1226
|
+
} else if (useCachedContext && entry && output.responseId) {
|
|
1227
|
+
const responseItems = convertResponsesMessages(model, { messages: [output] }, CODEX_TOOL_CALL_PROVIDERS, {
|
|
1228
|
+
includeSystemPrompt: false,
|
|
1229
|
+
}).filter((item) => typeof item === "object" && item !== null && (item as { type?: unknown }).type !== "function_call_output");
|
|
1230
|
+
entry.continuation = {
|
|
1231
|
+
lastRequestBody: fullBody,
|
|
1232
|
+
lastResponseId: output.responseId,
|
|
1233
|
+
lastResponseItems: responseItems,
|
|
1234
|
+
};
|
|
1163
1235
|
}
|
|
1164
1236
|
releaseOnce({ keep: keepConnection });
|
|
1165
1237
|
return;
|
|
1166
1238
|
} catch (error) {
|
|
1239
|
+
if (entry) {
|
|
1240
|
+
entry.continuation = undefined;
|
|
1241
|
+
}
|
|
1167
1242
|
keepConnection = false;
|
|
1168
1243
|
releaseOnce({ keep: false });
|
|
1169
1244
|
// Pi's stock provider reuses session WebSockets. In practice the Codex
|
|
@@ -1378,7 +1453,7 @@ function createCodexStream<TApi extends Api>(
|
|
|
1378
1453
|
const sseHeaders = buildSSEHeaders(model.headers, options?.headers, accountId, apiKey, options?.sessionId);
|
|
1379
1454
|
const websocketHeaders = buildWebSocketHeaders(model.headers, options?.headers, accountId, apiKey, websocketRequestId);
|
|
1380
1455
|
const bodyJson = JSON.stringify(body);
|
|
1381
|
-
const transport = options?.transport || "
|
|
1456
|
+
const transport = options?.transport || "auto";
|
|
1382
1457
|
|
|
1383
1458
|
if (transport !== "sse") {
|
|
1384
1459
|
let websocketStarted = false;
|
|
@@ -1406,7 +1481,17 @@ function createCodexStream<TApi extends Api>(
|
|
|
1406
1481
|
stream.end();
|
|
1407
1482
|
return;
|
|
1408
1483
|
} catch (error) {
|
|
1409
|
-
|
|
1484
|
+
appendAssistantMessageDiagnostic(
|
|
1485
|
+
output,
|
|
1486
|
+
createAssistantMessageDiagnostic("provider_transport_failure", error, {
|
|
1487
|
+
configuredTransport: transport,
|
|
1488
|
+
fallbackTransport: websocketStarted ? undefined : "sse",
|
|
1489
|
+
eventsEmitted: websocketStarted,
|
|
1490
|
+
phase: websocketStarted ? "after_message_stream_start" : "before_message_stream_start",
|
|
1491
|
+
requestBytes: new TextEncoder().encode(bodyJson).byteLength,
|
|
1492
|
+
}),
|
|
1493
|
+
);
|
|
1494
|
+
if (transport === "websocket" || transport === "websocket-cached" || websocketStarted) {
|
|
1410
1495
|
throw error;
|
|
1411
1496
|
}
|
|
1412
1497
|
}
|