@howaboua/pi-codex-conversion 1.0.20 → 1.0.21
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.21",
|
|
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
|
|
|
@@ -119,6 +118,7 @@ interface ResponsesBody {
|
|
|
119
118
|
input: unknown;
|
|
120
119
|
text: { verbosity: string };
|
|
121
120
|
include: string[];
|
|
121
|
+
max_output_tokens?: number;
|
|
122
122
|
prompt_cache_key?: string;
|
|
123
123
|
tool_choice: "auto";
|
|
124
124
|
parallel_tool_calls: boolean;
|
|
@@ -150,6 +150,8 @@ type ServiceTier = ResponseCreateParamsStreaming["service_tier"];
|
|
|
150
150
|
|
|
151
151
|
const websocketSessionCache = new Map<string, SessionWebSocketCacheEntry>();
|
|
152
152
|
|
|
153
|
+
class NonRetryableProviderError extends Error {}
|
|
154
|
+
|
|
153
155
|
interface StreamEventShape {
|
|
154
156
|
type?: string;
|
|
155
157
|
response?: ResponseEnvelope;
|
|
@@ -182,6 +184,11 @@ function shortenFilePart(value: string | undefined, fallback: string): string {
|
|
|
182
184
|
return `${prefix}${body.slice(0, 8)}-${body.slice(-4)}`;
|
|
183
185
|
}
|
|
184
186
|
|
|
187
|
+
function normalizeImageOutputFormat(value: string | undefined): string {
|
|
188
|
+
const format = (value ?? "png").toLowerCase();
|
|
189
|
+
return format === "png" || format === "jpg" || format === "jpeg" || format === "webp" ? format : "png";
|
|
190
|
+
}
|
|
191
|
+
|
|
185
192
|
function shortHash(str: string): string {
|
|
186
193
|
let h1 = 0xdeadbeef;
|
|
187
194
|
let h2 = 0x41c6ce57;
|
|
@@ -308,7 +315,7 @@ export function getOpenAICodexImageDirectory(cwd: string): string {
|
|
|
308
315
|
}
|
|
309
316
|
|
|
310
317
|
export function getOpenAICodexImagePath(cwd: string, responseId: string | undefined, callId: string, outputFormat?: string): string {
|
|
311
|
-
const ext = (outputFormat
|
|
318
|
+
const ext = normalizeImageOutputFormat(outputFormat);
|
|
312
319
|
const safeCallId = shortenFilePart(callId, "image");
|
|
313
320
|
const safeResponseId = shortenFilePart(responseId, "response");
|
|
314
321
|
return joinPaths(getOpenAICodexImageDirectory(cwd), `${safeCallId}-${safeResponseId}.${ext}`);
|
|
@@ -334,7 +341,8 @@ export async function saveOpenAICodexGeneratedImage(
|
|
|
334
341
|
const workspaceRoot = await resolveWorkspaceRoot(cwd);
|
|
335
342
|
const fs = await getNodeFsPromises();
|
|
336
343
|
const bytes = Buffer.from(image.result, "base64");
|
|
337
|
-
const
|
|
344
|
+
const outputFormat = normalizeImageOutputFormat(image.outputFormat);
|
|
345
|
+
const absolutePath = getOpenAICodexImagePath(workspaceRoot, image.responseId, image.callId, outputFormat);
|
|
338
346
|
const latestAbsolutePath = getOpenAICodexLatestImagePath(workspaceRoot);
|
|
339
347
|
await fs.mkdir(dirnamePath(absolutePath), { recursive: true });
|
|
340
348
|
await fs.writeFile(absolutePath, bytes);
|
|
@@ -353,7 +361,7 @@ export async function saveOpenAICodexGeneratedImage(
|
|
|
353
361
|
latestRelativePath: latestRelativePathValue,
|
|
354
362
|
responseId: image.responseId,
|
|
355
363
|
callId: image.callId,
|
|
356
|
-
outputFormat
|
|
364
|
+
outputFormat,
|
|
357
365
|
revisedPrompt: image.revisedPrompt,
|
|
358
366
|
};
|
|
359
367
|
}
|
|
@@ -465,7 +473,9 @@ function buildWebSocketHeaders(
|
|
|
465
473
|
|
|
466
474
|
function clampReasoningEffort(modelId: string, effort: string): string {
|
|
467
475
|
const id = modelId.includes("/") ? (modelId.split("/").pop() ?? modelId) : modelId;
|
|
468
|
-
|
|
476
|
+
const gpt5MinorMatch = /^gpt-5\.(\d+)/.exec(id);
|
|
477
|
+
const gpt5Minor = gpt5MinorMatch ? Number.parseInt(gpt5MinorMatch[1], 10) : undefined;
|
|
478
|
+
if (gpt5Minor !== undefined && gpt5Minor >= 2 && effort === "minimal") return "low";
|
|
469
479
|
if (id === "gpt-5.1" && effort === "xhigh") return "high";
|
|
470
480
|
if (id === "gpt-5.1-codex-mini") return effort === "high" || effort === "xhigh" ? "high" : "medium";
|
|
471
481
|
return effort;
|
|
@@ -517,6 +527,10 @@ function buildRequestBody<TApi extends Api>(model: Model<TApi>, context: Context
|
|
|
517
527
|
parallel_tool_calls: true,
|
|
518
528
|
};
|
|
519
529
|
|
|
530
|
+
if (options?.maxTokens !== undefined) {
|
|
531
|
+
body.max_output_tokens = options.maxTokens;
|
|
532
|
+
}
|
|
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;
|