@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 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.70.x`
12
+ - targets modern Pi tool/rendering APIs and is aligned with Pi `0.72.x`
13
13
 
14
14
  ![Available tools](./available-tools.png)
15
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howaboua/pi-codex-conversion",
3
- "version": "1.0.28",
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.70.0",
55
- "@mariozechner/pi-coding-agent": "^0.70.0",
56
- "@mariozechner/pi-tui": "^0.70.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.70.5",
61
- "@mariozechner/pi-coding-agent": "^0.70.5",
62
- "@mariozechner/pi-tui": "^0.70.5",
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
- input: unknown;
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
- if (options?.reasoning !== undefined) {
558
- const requested = supportsXhigh(model) ? options.reasoning : options.reasoning === "xhigh" ? "high" : options.reasoning;
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, requested),
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
- const cacheKey = buildWebSocketCacheKey(url, headers, sessionId);
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(cacheKey);
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(cacheKey);
799
+ websocketSessionCache.delete(sessionId);
795
800
  return;
796
801
  }
797
802
  cached.busy = false;
798
- scheduleSessionWebSocketExpiry(cacheKey, cached);
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(cacheKey);
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(cacheKey, entry);
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(cacheKey) === entry) {
831
- websocketSessionCache.delete(cacheKey);
836
+ if (websocketSessionCache.get(sessionId) === entry) {
837
+ websocketSessionCache.delete(sessionId);
832
838
  }
833
839
  return;
834
840
  }
835
841
  entry.busy = false;
836
- scheduleSessionWebSocketExpiry(cacheKey, entry);
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", ...body }));
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 || "sse";
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
- if (transport === "websocket" || websocketStarted) {
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
  }