@howaboua/pi-codex-conversion 1.0.28 → 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.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.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.70.0",
55
- "@mariozechner/pi-coding-agent": "^0.70.0",
56
- "@mariozechner/pi-tui": "^0.70.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.70.5",
61
- "@mariozechner/pi-coding-agent": "^0.70.5",
62
- "@mariozechner/pi-tui": "^0.70.5",
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
- input: unknown;
132
+ previous_response_id?: string;
133
+ input: unknown[];
125
134
  text: { verbosity: string };
126
135
  include: string[];
127
136
  prompt_cache_key?: string;
@@ -554,10 +563,13 @@ function buildRequestBody<TApi extends Api>(model: Model<TApi>, context: Context
554
563
  }
555
564
  }
556
565
 
557
- if (options?.reasoning !== undefined) {
558
- const requested = supportsXhigh(model) ? options.reasoning : options.reasoning === "xhigh" ? "high" : options.reasoning;
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, requested),
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", ...body }));
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
  }