@howaboua/pi-codex-conversion 1.0.20 → 1.0.23

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@howaboua/pi-codex-conversion",
3
- "version": "1.0.20",
3
+ "version": "1.0.23",
4
4
  "description": "Codex-oriented tool and prompt adapter for pi coding agent",
5
5
  "type": "module",
6
6
  "repository": {
@@ -68,7 +68,6 @@
68
68
  "node-pty": "^1.1.0",
69
69
  "partial-json": "^0.1.7",
70
70
  "tree-sitter-bash": "^0.25.1",
71
- "web-tree-sitter": "^0.26.7",
72
- "ws": "^8.20.0"
71
+ "web-tree-sitter": "^0.26.7"
73
72
  }
74
73
  }
@@ -105,7 +105,6 @@ interface SessionWebSocketCacheEntry {
105
105
  idleTimer?: ReturnType<typeof setTimeout>;
106
106
  }
107
107
 
108
- let webSocketConstructorPromise: Promise<WebSocketConstructorLike | null> | undefined;
109
108
  let fsPromisesPromise: Promise<typeof import("node:fs/promises")> | undefined;
110
109
  const workspaceRootCache = new Map<string, Promise<string>>();
111
110
 
@@ -150,6 +149,8 @@ type ServiceTier = ResponseCreateParamsStreaming["service_tier"];
150
149
 
151
150
  const websocketSessionCache = new Map<string, SessionWebSocketCacheEntry>();
152
151
 
152
+ class NonRetryableProviderError extends Error {}
153
+
153
154
  interface StreamEventShape {
154
155
  type?: string;
155
156
  response?: ResponseEnvelope;
@@ -182,6 +183,11 @@ function shortenFilePart(value: string | undefined, fallback: string): string {
182
183
  return `${prefix}${body.slice(0, 8)}-${body.slice(-4)}`;
183
184
  }
184
185
 
186
+ function normalizeImageOutputFormat(value: string | undefined): string {
187
+ const format = (value ?? "png").toLowerCase();
188
+ return format === "png" || format === "jpg" || format === "jpeg" || format === "webp" ? format : "png";
189
+ }
190
+
185
191
  function shortHash(str: string): string {
186
192
  let h1 = 0xdeadbeef;
187
193
  let h2 = 0x41c6ce57;
@@ -308,7 +314,7 @@ export function getOpenAICodexImageDirectory(cwd: string): string {
308
314
  }
309
315
 
310
316
  export function getOpenAICodexImagePath(cwd: string, responseId: string | undefined, callId: string, outputFormat?: string): string {
311
- const ext = (outputFormat ?? "png").toLowerCase();
317
+ const ext = normalizeImageOutputFormat(outputFormat);
312
318
  const safeCallId = shortenFilePart(callId, "image");
313
319
  const safeResponseId = shortenFilePart(responseId, "response");
314
320
  return joinPaths(getOpenAICodexImageDirectory(cwd), `${safeCallId}-${safeResponseId}.${ext}`);
@@ -334,7 +340,8 @@ export async function saveOpenAICodexGeneratedImage(
334
340
  const workspaceRoot = await resolveWorkspaceRoot(cwd);
335
341
  const fs = await getNodeFsPromises();
336
342
  const bytes = Buffer.from(image.result, "base64");
337
- const absolutePath = getOpenAICodexImagePath(workspaceRoot, image.responseId, image.callId, image.outputFormat);
343
+ const outputFormat = normalizeImageOutputFormat(image.outputFormat);
344
+ const absolutePath = getOpenAICodexImagePath(workspaceRoot, image.responseId, image.callId, outputFormat);
338
345
  const latestAbsolutePath = getOpenAICodexLatestImagePath(workspaceRoot);
339
346
  await fs.mkdir(dirnamePath(absolutePath), { recursive: true });
340
347
  await fs.writeFile(absolutePath, bytes);
@@ -353,7 +360,7 @@ export async function saveOpenAICodexGeneratedImage(
353
360
  latestRelativePath: latestRelativePathValue,
354
361
  responseId: image.responseId,
355
362
  callId: image.callId,
356
- outputFormat: (image.outputFormat ?? "png").toLowerCase(),
363
+ outputFormat,
357
364
  revisedPrompt: image.revisedPrompt,
358
365
  };
359
366
  }
@@ -465,7 +472,9 @@ function buildWebSocketHeaders(
465
472
 
466
473
  function clampReasoningEffort(modelId: string, effort: string): string {
467
474
  const id = modelId.includes("/") ? (modelId.split("/").pop() ?? modelId) : modelId;
468
- if ((id.startsWith("gpt-5.2") || id.startsWith("gpt-5.3") || id.startsWith("gpt-5.4")) && effort === "minimal") return "low";
475
+ const gpt5MinorMatch = /^gpt-5\.(\d+)/.exec(id);
476
+ const gpt5Minor = gpt5MinorMatch ? Number.parseInt(gpt5MinorMatch[1], 10) : undefined;
477
+ if (gpt5Minor !== undefined && gpt5Minor >= 2 && effort === "minimal") return "low";
469
478
  if (id === "gpt-5.1" && effort === "xhigh") return "high";
470
479
  if (id === "gpt-5.1-codex-mini") return effort === "high" || effort === "xhigh" ? "high" : "medium";
471
480
  return effort;
@@ -517,6 +526,11 @@ function buildRequestBody<TApi extends Api>(model: Model<TApi>, context: Context
517
526
  parallel_tool_calls: true,
518
527
  };
519
528
 
529
+ // The Codex ChatGPT-backed endpoint rejects output-token cap fields with
530
+ // `Unsupported parameter: max_output_tokens`. Pi's branch summarizer passes
531
+ // `maxTokens`, so forwarding it breaks `/tree` summaries and extensions that
532
+ // use `ctx.navigateTree(..., { summarize: true })`.
533
+
520
534
  if ((options as { temperature?: number } | undefined)?.temperature !== undefined) {
521
535
  body.temperature = (options as { temperature?: number }).temperature;
522
536
  }
@@ -620,27 +634,9 @@ async function* parseSSE(response: Response): AsyncIterable<StreamEventShape> {
620
634
  }
621
635
  }
622
636
 
623
- async function getWebSocketConstructor(): Promise<WebSocketConstructorLike | null> {
624
- if (webSocketConstructorPromise) {
625
- return webSocketConstructorPromise;
626
- }
627
-
628
- webSocketConstructorPromise = (async () => {
629
- const globalCtor = (globalThis as typeof globalThis & { WebSocket?: WebSocketConstructorLike }).WebSocket;
630
- if (typeof process === "undefined" || !(process.versions?.node || process.versions?.bun)) {
631
- return typeof globalCtor === "function" ? globalCtor : null;
632
- }
633
-
634
- try {
635
- const wsModule = (await dynamicImport("ws")) as { WebSocket?: WebSocketConstructorLike; default?: WebSocketConstructorLike };
636
- const ctor = wsModule.WebSocket ?? wsModule.default;
637
- return typeof ctor === "function" ? ctor : null;
638
- } catch {
639
- return typeof globalCtor === "function" ? globalCtor : null;
640
- }
641
- })();
642
-
643
- return webSocketConstructorPromise;
637
+ function getWebSocketConstructor(): WebSocketConstructorLike | null {
638
+ const ctor = (globalThis as typeof globalThis & { WebSocket?: WebSocketConstructorLike }).WebSocket;
639
+ return typeof ctor === "function" ? ctor : null;
644
640
  }
645
641
 
646
642
  function getWebSocketReadyState(socket: WebSocketLike): number | undefined {
@@ -693,20 +689,20 @@ function extractWebSocketCloseError(event: unknown): Error {
693
689
  }
694
690
 
695
691
  async function connectWebSocket(url: string, headers: Headers, signal: AbortSignal | undefined): Promise<WebSocketLike> {
696
- const WebSocketCtor = await getWebSocketConstructor();
692
+ const WebSocketCtor = getWebSocketConstructor();
697
693
  if (!WebSocketCtor) {
698
694
  throw new Error("WebSocket transport is not available in this runtime");
699
695
  }
700
696
 
701
697
  const wsHeaders = headersToRecord(headers);
698
+ delete wsHeaders["OpenAI-Beta"];
702
699
 
703
700
  return new Promise((resolve, reject) => {
704
701
  let settled = false;
705
702
  let socket: WebSocketLike;
706
- const isNodeLike = typeof process !== "undefined" && !!(process.versions?.node || process.versions?.bun);
707
703
 
708
704
  try {
709
- socket = isNodeLike ? new WebSocketCtor(url, { headers: wsHeaders }) : new WebSocketCtor(url);
705
+ socket = new WebSocketCtor(url, { headers: wsHeaders });
710
706
  } catch (error) {
711
707
  reject(error instanceof Error ? error : new Error(String(error)));
712
708
  return;
@@ -950,6 +946,7 @@ async function* parseWebSocket(socket: WebSocketLike, signal: AbortSignal | unde
950
946
  }
951
947
 
952
948
  async function* mapCodexEvents(events: AsyncIterable<StreamEventShape>): AsyncIterable<StreamEventShape> {
949
+ let sawTerminalResponse = false;
953
950
  for await (const event of events) {
954
951
  const type = typeof event.type === "string" ? event.type : undefined;
955
952
  if (!type) continue;
@@ -963,6 +960,7 @@ async function* mapCodexEvents(events: AsyncIterable<StreamEventShape>): AsyncIt
963
960
  }
964
961
 
965
962
  if (type === "response.done" || type === "response.completed" || type === "response.incomplete") {
963
+ sawTerminalResponse = true;
966
964
  const response = event.response;
967
965
  yield {
968
966
  ...event,
@@ -974,6 +972,10 @@ async function* mapCodexEvents(events: AsyncIterable<StreamEventShape>): AsyncIt
974
972
 
975
973
  yield event;
976
974
  }
975
+
976
+ if (!sawTerminalResponse) {
977
+ throw new Error("Stream closed before response.completed");
978
+ }
977
979
  }
978
980
 
979
981
  function normalizeCodexStatus(status: string | undefined): string | undefined {
@@ -1022,17 +1024,18 @@ async function* captureGeneratedImages(
1022
1024
  if (callId && result) {
1023
1025
  try {
1024
1026
  const outputFormat = typeof event.item.output_format === "string" ? event.item.output_format : undefined;
1027
+ const normalizedOutputFormat = normalizeImageOutputFormat(outputFormat);
1025
1028
  const saved = await saveOpenAICodexGeneratedImage(options.cwd, {
1026
1029
  responseId,
1027
1030
  callId,
1028
1031
  result,
1029
- outputFormat,
1032
+ outputFormat: normalizedOutputFormat,
1030
1033
  revisedPrompt:
1031
1034
  typeof event.item.revised_prompt === "string" ? event.item.revised_prompt : options.requestPrompt,
1032
1035
  });
1033
1036
  options.onImageSaved(saved, {
1034
1037
  data: result,
1035
- mimeType: `image/${(outputFormat ?? "png").toLowerCase()}`,
1038
+ mimeType: `image/${normalizedOutputFormat}`,
1036
1039
  });
1037
1040
  } catch (error) {
1038
1041
  console.warn("[pi-codex-conversion] Failed to save generated image", error);
@@ -1058,14 +1061,14 @@ async function processCapturedResponsesStream<TApi extends Api>(
1058
1061
  model: Model<TApi>,
1059
1062
  options: SimpleStreamOptions | undefined,
1060
1063
  deps: {
1061
- getCurrentCwd: () => string;
1062
1064
  onImageSaved?: (savedImage: SavedGeneratedImage, imageData: { data: string; mimeType: string }) => void;
1063
1065
  onWebSearchCaptured?: (search: SurfacedWebSearch) => void;
1064
1066
  },
1067
+ cwd: string,
1065
1068
  requestPrompt: string | undefined,
1066
1069
  ): Promise<void> {
1067
1070
  const tappedEvents = captureGeneratedImages(mapCodexEvents(events), {
1068
- cwd: deps.getCurrentCwd(),
1071
+ cwd,
1069
1072
  requestPrompt,
1070
1073
  onImageSaved: (image, imageData) => deps.onImageSaved?.(image, imageData),
1071
1074
  onWebSearchCaptured: (search) => deps.onWebSearchCaptured?.(search),
@@ -1088,10 +1091,10 @@ async function processWebSocketStream<TApi extends Api>(
1088
1091
  onStart: () => void,
1089
1092
  options: SimpleStreamOptions | undefined,
1090
1093
  deps: {
1091
- getCurrentCwd: () => string;
1092
1094
  onImageSaved?: (savedImage: SavedGeneratedImage, imageData: { data: string; mimeType: string }) => void;
1093
1095
  onWebSearchCaptured?: (search: SurfacedWebSearch) => void;
1094
1096
  },
1097
+ cwd: string,
1095
1098
  requestPrompt: string | undefined,
1096
1099
  ): Promise<void> {
1097
1100
  const { socket, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
@@ -1101,7 +1104,7 @@ async function processWebSocketStream<TApi extends Api>(
1101
1104
  socket.send(JSON.stringify({ type: "response.create", ...body }));
1102
1105
  onStart();
1103
1106
  stream.push({ type: "start", partial: output });
1104
- await processCapturedResponsesStream(parseWebSocket(socket, options?.signal), output, stream, model, options, deps, requestPrompt);
1107
+ await processCapturedResponsesStream(parseWebSocket(socket, options?.signal), output, stream, model, options, deps, cwd, requestPrompt);
1105
1108
  if (options?.signal?.aborted) {
1106
1109
  keepConnection = false;
1107
1110
  }
@@ -1280,6 +1283,7 @@ function createCodexStream<TApi extends Api>(
1280
1283
  },
1281
1284
  ): AssistantMessageEventStream {
1282
1285
  const stream = createAssistantMessageEventStream();
1286
+ const requestCwd = deps.getCurrentCwd();
1283
1287
 
1284
1288
  (async () => {
1285
1289
  const output = createInitialAssistantMessage(model);
@@ -1319,6 +1323,7 @@ function createCodexStream<TApi extends Api>(
1319
1323
  },
1320
1324
  options,
1321
1325
  deps,
1326
+ requestCwd,
1322
1327
  requestPrompt,
1323
1328
  );
1324
1329
  if (options?.signal?.aborted) {
@@ -1368,8 +1373,11 @@ function createCodexStream<TApi extends Api>(
1368
1373
  statusText: response.statusText,
1369
1374
  });
1370
1375
  const info = await parseErrorResponse(fakeResponse);
1371
- throw new Error(info.friendlyMessage || info.message);
1376
+ throw new NonRetryableProviderError(info.friendlyMessage || info.message);
1372
1377
  } catch (error) {
1378
+ if (error instanceof NonRetryableProviderError) {
1379
+ throw error;
1380
+ }
1373
1381
  if (error instanceof Error && (error.name === "AbortError" || error.message === "Request was aborted")) {
1374
1382
  throw new Error("Request was aborted");
1375
1383
  }
@@ -1392,7 +1400,7 @@ function createCodexStream<TApi extends Api>(
1392
1400
  }
1393
1401
 
1394
1402
  stream.push({ type: "start", partial: output });
1395
- await processCapturedResponsesStream(parseSSE(response), output, stream, model, options, deps, requestPrompt);
1403
+ await processCapturedResponsesStream(parseSSE(response), output, stream, model, options, deps, requestCwd, requestPrompt);
1396
1404
  finalizeUsage(model, output);
1397
1405
 
1398
1406
  if (options?.signal?.aborted) {
@@ -6,6 +6,23 @@ import type { AssistantMessageEventStream } from "@mariozechner/pi-ai";
6
6
  type MessageRole = Context["messages"][number]["role"];
7
7
  type Message = Context["messages"][number];
8
8
 
9
+ interface ImageGenerationCallItem {
10
+ type: "image_generation_call";
11
+ id?: string;
12
+ status?: string;
13
+ result?: string | null;
14
+ output_format?: string;
15
+ revised_prompt?: string;
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ interface ImageGenerationCallBlock {
20
+ type: "image_generation_call";
21
+ item: ImageGenerationCallItem;
22
+ }
23
+
24
+ type InternalAssistantContent = Extract<Message, { role: "assistant" }>["content"][number] | ImageGenerationCallBlock;
25
+
9
26
  export interface OpenAIResponsesStreamOptions {
10
27
  serviceTier?: ResponseCreateParamsStreaming["service_tier"];
11
28
  resolveServiceTier?: (
@@ -55,6 +72,10 @@ function sanitizeSurrogates(text: string): string {
55
72
  return text.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "");
56
73
  }
57
74
 
75
+ function isImageGenerationCallBlock(block: InternalAssistantContent): block is ImageGenerationCallBlock {
76
+ return block.type === "image_generation_call" && block.item?.type === "image_generation_call";
77
+ }
78
+
58
79
  const NON_VISION_USER_IMAGE_PLACEHOLDER = "(image omitted: model does not support images)";
59
80
  const NON_VISION_TOOL_IMAGE_PLACEHOLDER = "(tool image omitted: model does not support images)";
60
81
 
@@ -108,7 +129,8 @@ function transformMessages(
108
129
  const assistantMsg = msg;
109
130
  const isSameModel =
110
131
  assistantMsg.provider === model.provider && assistantMsg.api === model.api && assistantMsg.model === model.id;
111
- const transformedContent = assistantMsg.content.flatMap((block) => {
132
+ const transformedContent = (assistantMsg.content as InternalAssistantContent[]).flatMap((block) => {
133
+ if (isImageGenerationCallBlock(block)) return block;
112
134
  if (block.type === "thinking") {
113
135
  if (block.redacted) return isSameModel ? block : [];
114
136
  if (isSameModel && block.thinkingSignature) return block;
@@ -133,7 +155,7 @@ function transformMessages(
133
155
  }
134
156
  return block;
135
157
  });
136
- return { ...assistantMsg, content: transformedContent };
158
+ return { ...assistantMsg, content: transformedContent as Extract<Message, { role: "assistant" }>["content"] };
137
159
  }
138
160
  return msg;
139
161
  });
@@ -263,8 +285,10 @@ export function convertResponsesMessages<TApi extends Api>(
263
285
  const output: ResponseInput = [];
264
286
  const isDifferentModel = msg.model !== model.id && msg.provider === model.provider && msg.api === model.api;
265
287
  let assistantBlockIndex = 0;
266
- for (const block of msg.content) {
267
- if (block.type === "thinking") {
288
+ for (const block of msg.content as InternalAssistantContent[]) {
289
+ if (isImageGenerationCallBlock(block)) {
290
+ output.push(block.item as ResponseInput[number]);
291
+ } else if (block.type === "thinking") {
268
292
  if (block.thinkingSignature) output.push(JSON.parse(block.thinkingSignature));
269
293
  } else if (block.type === "text") {
270
294
  const parsedSignature = parseTextSignature(block.textSignature);
@@ -556,6 +580,12 @@ export async function processResponsesStream<TApi extends Api>(
556
580
  const toolCallIndex = state?.kind === "function_call" ? state.blockIndex : blockIndex();
557
581
  stream.push({ type: "toolcall_end", contentIndex: toolCallIndex, toolCall, partial: output });
558
582
  outputStates.delete(event.output_index);
583
+ } else if (item.type === "image_generation_call") {
584
+ (output.content as InternalAssistantContent[]).push({
585
+ type: "image_generation_call",
586
+ item: item as ImageGenerationCallItem,
587
+ });
588
+ outputStates.delete(event.output_index);
559
589
  }
560
590
  } else if (event.type === "response.completed") {
561
591
  const response = event.response;