@howaboua/pi-codex-conversion 1.0.27 → 1.0.29
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
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.29",
|
|
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.72.0",
|
|
55
|
+
"@mariozechner/pi-coding-agent": "^0.72.0",
|
|
56
|
+
"@mariozechner/pi-tui": "^0.72.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.72.0",
|
|
61
|
+
"@mariozechner/pi-coding-agent": "^0.72.0",
|
|
62
|
+
"@mariozechner/pi-tui": "^0.72.0",
|
|
63
63
|
"tsx": "^4.20.5",
|
|
64
64
|
"typebox": "^1.1.24",
|
|
65
65
|
"typescript": "^5.9.3"
|
|
@@ -2,8 +2,8 @@ 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
|
+
clampThinkingLevel,
|
|
5
6
|
getEnvApiKey,
|
|
6
|
-
supportsXhigh,
|
|
7
7
|
type Api,
|
|
8
8
|
type AssistantMessage,
|
|
9
9
|
type AssistantMessageEventStream,
|
|
@@ -103,14 +103,22 @@ interface SessionWebSocketCacheEntry {
|
|
|
103
103
|
socket: WebSocketLike;
|
|
104
104
|
busy: boolean;
|
|
105
105
|
idleTimer?: ReturnType<typeof setTimeout>;
|
|
106
|
+
continuation?: CachedWebSocketContinuationState;
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
interface AcquiredWebSocket {
|
|
109
110
|
socket: WebSocketLike;
|
|
111
|
+
entry?: SessionWebSocketCacheEntry;
|
|
110
112
|
reused: boolean;
|
|
111
113
|
release: (options?: { keep?: boolean }) => void;
|
|
112
114
|
}
|
|
113
115
|
|
|
116
|
+
interface CachedWebSocketContinuationState {
|
|
117
|
+
lastRequestBody: ResponsesBody;
|
|
118
|
+
lastResponseId: string;
|
|
119
|
+
lastResponseItems: unknown[];
|
|
120
|
+
}
|
|
121
|
+
|
|
114
122
|
let fsPromisesPromise: Promise<typeof import("node:fs/promises")> | undefined;
|
|
115
123
|
const workspaceRootCache = new Map<string, Promise<string>>();
|
|
116
124
|
|
|
@@ -121,7 +129,8 @@ interface ResponsesBody {
|
|
|
121
129
|
store: boolean;
|
|
122
130
|
stream: boolean;
|
|
123
131
|
instructions?: string;
|
|
124
|
-
|
|
132
|
+
previous_response_id?: string;
|
|
133
|
+
input: unknown[];
|
|
125
134
|
text: { verbosity: string };
|
|
126
135
|
include: string[];
|
|
127
136
|
prompt_cache_key?: string;
|
|
@@ -525,7 +534,7 @@ function buildRequestBody<TApi extends Api>(model: Model<TApi>, context: Context
|
|
|
525
534
|
stream: true,
|
|
526
535
|
instructions: context.systemPrompt,
|
|
527
536
|
input: messages,
|
|
528
|
-
text: { verbosity: ((options as { textVerbosity?: string } | undefined)?.textVerbosity ?? "
|
|
537
|
+
text: { verbosity: ((options as { textVerbosity?: string } | undefined)?.textVerbosity ?? "low") as string },
|
|
529
538
|
include: ["reasoning.encrypted_content"],
|
|
530
539
|
prompt_cache_key: options?.sessionId,
|
|
531
540
|
tool_choice: "auto",
|
|
@@ -546,7 +555,7 @@ function buildRequestBody<TApi extends Api>(model: Model<TApi>, context: Context
|
|
|
546
555
|
body.service_tier = serviceTier;
|
|
547
556
|
}
|
|
548
557
|
|
|
549
|
-
if (context.tools) {
|
|
558
|
+
if (context.tools && context.tools.length > 0) {
|
|
550
559
|
body.tools = convertResponsesTools(context.tools, { strict: null });
|
|
551
560
|
const hasWebSearchTool = context.tools.some((tool) => tool.name === "web_search");
|
|
552
561
|
if (hasWebSearchTool) {
|
|
@@ -554,10 +563,13 @@ function buildRequestBody<TApi extends Api>(model: Model<TApi>, context: Context
|
|
|
554
563
|
}
|
|
555
564
|
}
|
|
556
565
|
|
|
557
|
-
|
|
558
|
-
|
|
566
|
+
const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
|
|
567
|
+
const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
|
|
568
|
+
if (reasoningEffort !== undefined) {
|
|
569
|
+
const effort = model.thinkingLevelMap?.[reasoningEffort] ?? reasoningEffort;
|
|
570
|
+
if (effort === null) return body;
|
|
559
571
|
body.reasoning = {
|
|
560
|
-
effort: clampReasoningEffort(model.id,
|
|
572
|
+
effort: clampReasoningEffort(model.id, effort),
|
|
561
573
|
summary: ((options as { reasoningSummary?: string } | undefined)?.reasoningSummary ?? "auto") as string,
|
|
562
574
|
};
|
|
563
575
|
}
|
|
@@ -787,6 +799,7 @@ async function acquireWebSocket(
|
|
|
787
799
|
cached.busy = true;
|
|
788
800
|
return {
|
|
789
801
|
socket: cached.socket,
|
|
802
|
+
entry: cached,
|
|
790
803
|
reused: true,
|
|
791
804
|
release: ({ keep } = {}) => {
|
|
792
805
|
if (!keep || !isWebSocketReusable(cached.socket)) {
|
|
@@ -822,6 +835,7 @@ async function acquireWebSocket(
|
|
|
822
835
|
websocketSessionCache.set(cacheKey, entry);
|
|
823
836
|
return {
|
|
824
837
|
socket,
|
|
838
|
+
entry,
|
|
825
839
|
reused: false,
|
|
826
840
|
release: ({ keep } = {}) => {
|
|
827
841
|
if (!keep || !isWebSocketReusable(entry.socket)) {
|
|
@@ -853,6 +867,57 @@ async function decodeWebSocketData(data: unknown): Promise<string | null> {
|
|
|
853
867
|
return null;
|
|
854
868
|
}
|
|
855
869
|
|
|
870
|
+
function requestBodyWithoutInput(body: ResponsesBody): ResponsesBody {
|
|
871
|
+
const { input: _input, previous_response_id: _previousResponseId, ...rest } = body;
|
|
872
|
+
return rest as ResponsesBody;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function responseInputsEqual(a: unknown[] | undefined, b: unknown[] | undefined): boolean {
|
|
876
|
+
return JSON.stringify(a ?? []) === JSON.stringify(b ?? []);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function requestBodiesMatchExceptInput(a: ResponsesBody, b: ResponsesBody): boolean {
|
|
880
|
+
return JSON.stringify(requestBodyWithoutInput(a)) === JSON.stringify(requestBodyWithoutInput(b));
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function getCachedWebSocketInputDelta(body: ResponsesBody, continuation: CachedWebSocketContinuationState): unknown[] | undefined {
|
|
884
|
+
if (!requestBodiesMatchExceptInput(body, continuation.lastRequestBody)) {
|
|
885
|
+
return undefined;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const currentInput = body.input ?? [];
|
|
889
|
+
const baseline = [...(continuation.lastRequestBody.input ?? []), ...continuation.lastResponseItems];
|
|
890
|
+
if (currentInput.length < baseline.length) {
|
|
891
|
+
return undefined;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const prefix = currentInput.slice(0, baseline.length);
|
|
895
|
+
if (!responseInputsEqual(prefix, baseline)) {
|
|
896
|
+
return undefined;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return currentInput.slice(baseline.length);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function buildCachedWebSocketRequestBody(entry: SessionWebSocketCacheEntry, body: ResponsesBody): ResponsesBody {
|
|
903
|
+
const continuation = entry.continuation;
|
|
904
|
+
if (!continuation) {
|
|
905
|
+
return body;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const delta = getCachedWebSocketInputDelta(body, continuation);
|
|
909
|
+
if (!delta || !continuation.lastResponseId) {
|
|
910
|
+
entry.continuation = undefined;
|
|
911
|
+
return body;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return {
|
|
915
|
+
...body,
|
|
916
|
+
previous_response_id: continuation.lastResponseId,
|
|
917
|
+
input: delta,
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
|
|
856
921
|
async function* parseWebSocket(socket: WebSocketLike, signal: AbortSignal | undefined): AsyncIterable<StreamEventShape> {
|
|
857
922
|
const queue: StreamEventShape[] = [];
|
|
858
923
|
let pending: (() => void) | null = null;
|
|
@@ -1128,10 +1193,15 @@ async function processWebSocketStream<TApi extends Api>(
|
|
|
1128
1193
|
let streamStarted = false;
|
|
1129
1194
|
|
|
1130
1195
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1131
|
-
const { socket, release, reused } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
|
|
1196
|
+
const { socket, entry, release, reused } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
|
|
1132
1197
|
let keepConnection = true;
|
|
1133
1198
|
let released = false;
|
|
1134
1199
|
let eventCount = 0;
|
|
1200
|
+
const useCachedContext = (options as { transport?: string } | undefined)?.transport === "websocket-cached";
|
|
1201
|
+
// ChatGPT Codex Responses rejects `store: true` ("Store must be set to false").
|
|
1202
|
+
// WebSocket continuation still works via connection-scoped previous_response_id state.
|
|
1203
|
+
const fullBody = body;
|
|
1204
|
+
const requestBody = useCachedContext && entry ? buildCachedWebSocketRequestBody(entry, fullBody) : fullBody;
|
|
1135
1205
|
|
|
1136
1206
|
const releaseOnce = (releaseOptions?: { keep?: boolean }) => {
|
|
1137
1207
|
if (released) return;
|
|
@@ -1140,7 +1210,7 @@ async function processWebSocketStream<TApi extends Api>(
|
|
|
1140
1210
|
};
|
|
1141
1211
|
|
|
1142
1212
|
try {
|
|
1143
|
-
socket.send(JSON.stringify({ type: "response.create", ...
|
|
1213
|
+
socket.send(JSON.stringify({ type: "response.create", ...requestBody }));
|
|
1144
1214
|
if (!streamStarted) {
|
|
1145
1215
|
onStart();
|
|
1146
1216
|
stream.push({ type: "start", partial: output });
|
|
@@ -1160,10 +1230,22 @@ async function processWebSocketStream<TApi extends Api>(
|
|
|
1160
1230
|
);
|
|
1161
1231
|
if (options?.signal?.aborted) {
|
|
1162
1232
|
keepConnection = false;
|
|
1233
|
+
} else if (useCachedContext && entry && output.responseId) {
|
|
1234
|
+
const responseItems = convertResponsesMessages(model, { messages: [output] }, CODEX_TOOL_CALL_PROVIDERS, {
|
|
1235
|
+
includeSystemPrompt: false,
|
|
1236
|
+
}).filter((item) => typeof item === "object" && item !== null && (item as { type?: unknown }).type !== "function_call_output");
|
|
1237
|
+
entry.continuation = {
|
|
1238
|
+
lastRequestBody: fullBody,
|
|
1239
|
+
lastResponseId: output.responseId,
|
|
1240
|
+
lastResponseItems: responseItems,
|
|
1241
|
+
};
|
|
1163
1242
|
}
|
|
1164
1243
|
releaseOnce({ keep: keepConnection });
|
|
1165
1244
|
return;
|
|
1166
1245
|
} catch (error) {
|
|
1246
|
+
if (entry) {
|
|
1247
|
+
entry.continuation = undefined;
|
|
1248
|
+
}
|
|
1167
1249
|
keepConnection = false;
|
|
1168
1250
|
releaseOnce({ keep: false });
|
|
1169
1251
|
// Pi's stock provider reuses session WebSockets. In practice the Codex
|
|
@@ -1406,7 +1488,7 @@ function createCodexStream<TApi extends Api>(
|
|
|
1406
1488
|
stream.end();
|
|
1407
1489
|
return;
|
|
1408
1490
|
} catch (error) {
|
|
1409
|
-
if (transport === "websocket" || websocketStarted) {
|
|
1491
|
+
if (transport === "websocket" || transport === "websocket-cached" || websocketStarted) {
|
|
1410
1492
|
throw error;
|
|
1411
1493
|
}
|
|
1412
1494
|
}
|