@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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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 =
|
|
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 =
|
|
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/${
|
|
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
|
|
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
|
|
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
|
|
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;
|