pi-openai-codex-compat 0.0.3 → 0.0.4
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/CHANGELOG.md +36 -0
- package/README.md +71 -37
- package/extensions/openai-codex-compat/apply-patch-diff-render.ts +6 -2
- package/extensions/openai-codex-compat/apply-patch-engine.ts +89 -5
- package/extensions/openai-codex-compat/codex-cache-diagnostics.ts +97 -0
- package/extensions/openai-codex-compat/codex-installation.ts +51 -0
- package/extensions/openai-codex-compat/codex-metadata.ts +139 -0
- package/extensions/openai-codex-compat/codex-protocol.ts +1 -0
- package/extensions/openai-codex-compat/codex-provider.ts +524 -58
- package/extensions/openai-codex-compat/codex-stream.ts +52 -26
- package/extensions/openai-codex-compat/codex-thread-lineage.ts +156 -0
- package/extensions/openai-codex-compat/codex-transport.ts +1129 -86
- package/extensions/openai-codex-compat/compaction-checkpoint.ts +2 -2
- package/extensions/openai-codex-compat/config.ts +13 -0
- package/extensions/openai-codex-compat/index.ts +6 -0
- package/extensions/openai-codex-compat/namespaced-tools.ts +2 -0
- package/extensions/openai-codex-compat/output-limit-continuation.ts +151 -0
- package/extensions/openai-codex-compat/remote-compaction.ts +13 -0
- package/extensions/openai-codex-compat/request-options.ts +2 -2
- package/extensions/openai-codex-compat/responses-lite.ts +147 -0
- package/extensions/openai-codex-compat/settings-pane.ts +11 -0
- package/package.json +2 -2
|
@@ -41,6 +41,12 @@ type ToolCallSlot = Extract<OutputSlot, { type: "toolCall" }>;
|
|
|
41
41
|
|
|
42
42
|
type ProcessCodexStreamOptions = {
|
|
43
43
|
applyServiceTierPricing?(usage: Usage, responseServiceTier: string | undefined): void;
|
|
44
|
+
attemptState?: CodexStreamAttemptState;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type CodexStreamAttemptState = {
|
|
48
|
+
startedContentIndexes: Set<number>;
|
|
49
|
+
completedContentIndexes: Set<number>;
|
|
44
50
|
};
|
|
45
51
|
|
|
46
52
|
type CodexResponseStatus =
|
|
@@ -68,6 +74,18 @@ function stringValue(value: unknown): string {
|
|
|
68
74
|
return typeof value === "string" ? value : "";
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
function piToolCallName(item: JsonRecord): string {
|
|
78
|
+
const wireName = stringValue(item.name);
|
|
79
|
+
const name =
|
|
80
|
+
item["namespace"] === undefined
|
|
81
|
+
? wireName
|
|
82
|
+
: namespacedToolCallName(item["namespace"], wireName);
|
|
83
|
+
if (item["namespace"] === undefined && CODEX_NAMESPACED_TOOL_NAMES.has(name)) {
|
|
84
|
+
throw new Error(`Codex returned namespaced tool "${name}" as a flat function call.`);
|
|
85
|
+
}
|
|
86
|
+
return name;
|
|
87
|
+
}
|
|
88
|
+
|
|
71
89
|
function encodeTextSignature(id: string, phase: unknown): string {
|
|
72
90
|
const payload: TextSignatureV1 = { v: 1, id };
|
|
73
91
|
if (phase === "commentary" || phase === "final_answer") payload.phase = phase;
|
|
@@ -216,15 +234,24 @@ export async function processCodexStream(
|
|
|
216
234
|
});
|
|
217
235
|
};
|
|
218
236
|
|
|
237
|
+
const trackStarted = <TSlot extends OutputSlot>(slot: TSlot): TSlot => {
|
|
238
|
+
options?.attemptState?.startedContentIndexes.add(slot.contentIndex);
|
|
239
|
+
return slot;
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const trackCompleted = (slot: OutputSlot): void => {
|
|
243
|
+
options?.attemptState?.completedContentIndexes.add(slot.contentIndex);
|
|
244
|
+
};
|
|
245
|
+
|
|
219
246
|
const createSlot = (index: number, item: JsonRecord): OutputSlot | undefined => {
|
|
220
247
|
if (item.type === "reasoning") {
|
|
221
248
|
const block: ThinkingContent = { type: "thinking", thinking: "" };
|
|
222
249
|
output.content.push(block);
|
|
223
|
-
const slot = {
|
|
250
|
+
const slot = trackStarted({
|
|
224
251
|
type: "thinking",
|
|
225
252
|
block,
|
|
226
253
|
contentIndex: output.content.length - 1,
|
|
227
|
-
} satisfies OutputSlot;
|
|
254
|
+
} satisfies OutputSlot);
|
|
228
255
|
slots.set(index, slot);
|
|
229
256
|
stream.push({ type: "thinking_start", contentIndex: slot.contentIndex, partial: output });
|
|
230
257
|
return slot;
|
|
@@ -233,24 +260,17 @@ export async function processCodexStream(
|
|
|
233
260
|
applyMessagePhaseStopReason(item);
|
|
234
261
|
const block: TextContent = { type: "text", text: "" };
|
|
235
262
|
output.content.push(block);
|
|
236
|
-
const slot = {
|
|
263
|
+
const slot = trackStarted({
|
|
237
264
|
type: "text",
|
|
238
265
|
block,
|
|
239
266
|
contentIndex: output.content.length - 1,
|
|
240
|
-
} satisfies OutputSlot;
|
|
267
|
+
} satisfies OutputSlot);
|
|
241
268
|
slots.set(index, slot);
|
|
242
269
|
stream.push({ type: "text_start", contentIndex: slot.contentIndex, partial: output });
|
|
243
270
|
return slot;
|
|
244
271
|
}
|
|
245
272
|
if (item.type === "function_call") {
|
|
246
|
-
const
|
|
247
|
-
const name =
|
|
248
|
-
item["namespace"] === undefined
|
|
249
|
-
? wireName
|
|
250
|
-
: namespacedToolCallName(item["namespace"], wireName);
|
|
251
|
-
if (item["namespace"] === undefined && CODEX_NAMESPACED_TOOL_NAMES.has(name)) {
|
|
252
|
-
throw new Error(`Codex returned namespaced tool "${name}" as a flat function call.`);
|
|
253
|
-
}
|
|
273
|
+
const name = piToolCallName(item);
|
|
254
274
|
const block: StreamingToolCall = {
|
|
255
275
|
type: "toolCall",
|
|
256
276
|
id: `${stringValue(item["call_id"])}|${stringValue(item.id)}`,
|
|
@@ -259,17 +279,17 @@ export async function processCodexStream(
|
|
|
259
279
|
partialJson: typeof item.arguments === "string" ? item.arguments : "",
|
|
260
280
|
};
|
|
261
281
|
output.content.push(block);
|
|
262
|
-
const slot = {
|
|
282
|
+
const slot = trackStarted({
|
|
263
283
|
type: "toolCall",
|
|
264
284
|
block,
|
|
265
285
|
contentIndex: output.content.length - 1,
|
|
266
|
-
} satisfies OutputSlot;
|
|
286
|
+
} satisfies OutputSlot);
|
|
267
287
|
slots.set(index, slot);
|
|
268
288
|
stream.push({ type: "toolcall_start", contentIndex: slot.contentIndex, partial: output });
|
|
269
289
|
return slot;
|
|
270
290
|
}
|
|
271
291
|
if (item.type === "custom_tool_call") {
|
|
272
|
-
const name =
|
|
292
|
+
const name = piToolCallName(item);
|
|
273
293
|
const property = grammarToolInputProperties.get(name) ?? "input";
|
|
274
294
|
const input = typeof item["input"] === "string" ? item["input"] : "";
|
|
275
295
|
const block: StreamingToolCall = {
|
|
@@ -283,11 +303,11 @@ export async function processCodexStream(
|
|
|
283
303
|
},
|
|
284
304
|
};
|
|
285
305
|
output.content.push(block);
|
|
286
|
-
const slot = {
|
|
306
|
+
const slot = trackStarted({
|
|
287
307
|
type: "toolCall",
|
|
288
308
|
block,
|
|
289
309
|
contentIndex: output.content.length - 1,
|
|
290
|
-
} satisfies OutputSlot;
|
|
310
|
+
} satisfies OutputSlot);
|
|
291
311
|
slots.set(index, slot);
|
|
292
312
|
stream.push({ type: "toolcall_start", contentIndex: slot.contentIndex, partial: output });
|
|
293
313
|
return slot;
|
|
@@ -448,6 +468,7 @@ export async function processCodexStream(
|
|
|
448
468
|
content: slot.block.thinking,
|
|
449
469
|
partial: output,
|
|
450
470
|
});
|
|
471
|
+
trackCompleted(slot);
|
|
451
472
|
slots.delete(index);
|
|
452
473
|
} else if (item.type === "message" && slot?.type === "text") {
|
|
453
474
|
slot.block.text = itemContentText(item);
|
|
@@ -460,17 +481,14 @@ export async function processCodexStream(
|
|
|
460
481
|
content: slot.block.text,
|
|
461
482
|
partial: output,
|
|
462
483
|
});
|
|
484
|
+
trackCompleted(slot);
|
|
463
485
|
slots.delete(index);
|
|
464
486
|
} else if (
|
|
465
487
|
item.type === "function_call" &&
|
|
466
488
|
slot?.type === "toolCall" &&
|
|
467
489
|
slot.block.partialJson !== undefined
|
|
468
490
|
) {
|
|
469
|
-
|
|
470
|
-
slot.block.name = namespacedToolCallName(item["namespace"], item.name);
|
|
471
|
-
} else if (typeof item.name === "string" && CODEX_NAMESPACED_TOOL_NAMES.has(item.name)) {
|
|
472
|
-
throw new Error(`Codex returned namespaced tool "${item.name}" as a flat function call.`);
|
|
473
|
-
}
|
|
491
|
+
slot.block.name = piToolCallName(item);
|
|
474
492
|
const argumentsJson =
|
|
475
493
|
typeof item.arguments === "string" ? item.arguments : slot.block.partialJson || "{}";
|
|
476
494
|
slot.block.arguments = parseStreamingJson(argumentsJson);
|
|
@@ -481,8 +499,10 @@ export async function processCodexStream(
|
|
|
481
499
|
toolCall: slot.block,
|
|
482
500
|
partial: output,
|
|
483
501
|
});
|
|
502
|
+
trackCompleted(slot);
|
|
484
503
|
slots.delete(index);
|
|
485
504
|
} else if (item.type === "custom_tool_call" && slot?.type === "toolCall") {
|
|
505
|
+
slot.block.name = piToolCallName(item);
|
|
486
506
|
const input = typeof item["input"] === "string" ? item["input"] : customInput(slot.block);
|
|
487
507
|
pushToolDelta(slot, appendCustomInput(slot.block, input, true));
|
|
488
508
|
delete slot.block.customInput;
|
|
@@ -492,6 +512,7 @@ export async function processCodexStream(
|
|
|
492
512
|
toolCall: slot.block,
|
|
493
513
|
partial: output,
|
|
494
514
|
});
|
|
515
|
+
trackCompleted(slot);
|
|
495
516
|
slots.delete(index);
|
|
496
517
|
}
|
|
497
518
|
} else if (
|
|
@@ -500,12 +521,17 @@ export async function processCodexStream(
|
|
|
500
521
|
) {
|
|
501
522
|
finalize(event.response);
|
|
502
523
|
} else if (event.type === "response.failed") {
|
|
503
|
-
terminal = true;
|
|
504
524
|
const response = isObject(event.response) ? event.response : undefined;
|
|
525
|
+
if (response) {
|
|
526
|
+
finalize(response);
|
|
527
|
+
} else {
|
|
528
|
+
terminal = true;
|
|
529
|
+
}
|
|
530
|
+
output.stopReason = "error";
|
|
531
|
+
output.rawStopReason ??= "failed";
|
|
505
532
|
const error = isObject(response?.["error"]) ? response["error"] : undefined;
|
|
506
|
-
|
|
507
|
-
typeof error?.["message"] === "string" ? error["message"] : "Codex response failed"
|
|
508
|
-
);
|
|
533
|
+
output.errorMessage =
|
|
534
|
+
typeof error?.["message"] === "string" ? error["message"] : "Codex response failed";
|
|
509
535
|
}
|
|
510
536
|
}
|
|
511
537
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { uuidv7 } from "@earendil-works/pi-ai";
|
|
3
|
+
import { codexCacheKey } from "./codex-cache-key.ts";
|
|
4
|
+
|
|
5
|
+
export const CODEX_THREAD_MARKER_ENTRY_TYPE = "openai-codex-compat-thread";
|
|
6
|
+
|
|
7
|
+
export type CodexThreadMarkerData = {
|
|
8
|
+
version: 1;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
threadId: string;
|
|
11
|
+
forkedFromThreadId: string;
|
|
12
|
+
branchParentEntryId: string | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type CodexThreadIdentity = {
|
|
16
|
+
threadId: string;
|
|
17
|
+
forkedFromThreadId?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type PendingTreeFork = {
|
|
21
|
+
expectedLeafId: string | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function markerData(entry: SessionEntry, sessionId: string): CodexThreadMarkerData | undefined {
|
|
25
|
+
if (entry.type !== "custom" || entry.customType !== CODEX_THREAD_MARKER_ENTRY_TYPE) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const data = entry.data;
|
|
29
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
30
|
+
throw new Error("The active Pi branch contains an invalid OpenAI Codex thread marker.");
|
|
31
|
+
}
|
|
32
|
+
const candidate = data as Record<string, unknown>;
|
|
33
|
+
if (
|
|
34
|
+
candidate["version"] !== 1 ||
|
|
35
|
+
typeof candidate["sessionId"] !== "string" ||
|
|
36
|
+
candidate["sessionId"].length === 0 ||
|
|
37
|
+
typeof candidate["threadId"] !== "string" ||
|
|
38
|
+
candidate["threadId"].length === 0 ||
|
|
39
|
+
typeof candidate["forkedFromThreadId"] !== "string" ||
|
|
40
|
+
candidate["forkedFromThreadId"].length === 0 ||
|
|
41
|
+
(candidate["branchParentEntryId"] !== null &&
|
|
42
|
+
typeof candidate["branchParentEntryId"] !== "string")
|
|
43
|
+
) {
|
|
44
|
+
throw new Error("The active Pi branch contains an invalid OpenAI Codex thread marker.");
|
|
45
|
+
}
|
|
46
|
+
if (candidate["sessionId"] !== sessionId) return undefined;
|
|
47
|
+
if (entry.parentId !== candidate["branchParentEntryId"]) {
|
|
48
|
+
throw new Error("The active Pi branch contains a misplaced OpenAI Codex thread marker.");
|
|
49
|
+
}
|
|
50
|
+
return candidate as CodexThreadMarkerData;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function latestMarkerIndex(sessionId: string, branch: readonly SessionEntry[]): number {
|
|
54
|
+
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
55
|
+
if (markerData(branch[index]!, sessionId)) return index;
|
|
56
|
+
}
|
|
57
|
+
return -1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resolveCodexThreadIdentity(
|
|
61
|
+
sessionId: string,
|
|
62
|
+
branch: readonly SessionEntry[],
|
|
63
|
+
): CodexThreadIdentity {
|
|
64
|
+
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
65
|
+
const marker = markerData(branch[index]!, sessionId);
|
|
66
|
+
if (marker) {
|
|
67
|
+
return {
|
|
68
|
+
threadId: marker.threadId,
|
|
69
|
+
forkedFromThreadId: marker.forkedFromThreadId,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { threadId: codexCacheKey(sessionId)! };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function shouldForkOnNextAppend(
|
|
77
|
+
sessionId: string,
|
|
78
|
+
branch: readonly SessionEntry[],
|
|
79
|
+
entries: readonly SessionEntry[],
|
|
80
|
+
): boolean {
|
|
81
|
+
// Pi does not persist an explicit branch-start flag. Its append order is
|
|
82
|
+
// stable, so the first child inherits its thread and later children are
|
|
83
|
+
// forks unless the active path already contains a marker for that fork.
|
|
84
|
+
const firstChildByParent = new Map<string | null, string>();
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
if (!firstChildByParent.has(entry.parentId)) {
|
|
87
|
+
firstChildByParent.set(entry.parentId, entry.id);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const markerIndex = latestMarkerIndex(sessionId, branch);
|
|
92
|
+
for (let index = markerIndex + 1; index < branch.length; index += 1) {
|
|
93
|
+
const entry = branch[index]!;
|
|
94
|
+
if (firstChildByParent.get(entry.parentId) !== entry.id) return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const leafId = branch.at(-1)?.id ?? null;
|
|
98
|
+
return entries.some((entry) => entry.parentId === leafId);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function armPendingFork(pending: Map<string, PendingTreeFork>, ctx: ExtensionContext): void {
|
|
102
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
103
|
+
const branch = ctx.sessionManager.getBranch() as SessionEntry[];
|
|
104
|
+
const entries = ctx.sessionManager.getEntries() as SessionEntry[];
|
|
105
|
+
if (!shouldForkOnNextAppend(sessionId, branch, entries)) {
|
|
106
|
+
pending.delete(sessionId);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
pending.set(sessionId, { expectedLeafId: ctx.sessionManager.getLeafId() });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function pendingLeafIsActive(pending: PendingTreeFork, branch: readonly SessionEntry[]): boolean {
|
|
113
|
+
return (
|
|
114
|
+
pending.expectedLeafId === null || branch.some((entry) => entry.id === pending.expectedLeafId)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export default function registerCodexThreadLineage(pi: ExtensionAPI): void {
|
|
119
|
+
const pending = new Map<string, PendingTreeFork>();
|
|
120
|
+
|
|
121
|
+
pi.on("session_start", (_event, ctx) => {
|
|
122
|
+
armPendingFork(pending, ctx);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
126
|
+
armPendingFork(pending, ctx);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
pi.on("message_end", (event, ctx) => {
|
|
130
|
+
if (event.message.role !== "user") return;
|
|
131
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
132
|
+
const candidate = pending.get(sessionId);
|
|
133
|
+
if (!candidate) return;
|
|
134
|
+
pending.delete(sessionId);
|
|
135
|
+
|
|
136
|
+
const branch = ctx.sessionManager.getBranch() as SessionEntry[];
|
|
137
|
+
if (!pendingLeafIsActive(candidate, branch)) return;
|
|
138
|
+
|
|
139
|
+
const parent = resolveCodexThreadIdentity(sessionId, branch);
|
|
140
|
+
const branchParentEntryId = ctx.sessionManager.getLeafId();
|
|
141
|
+
// Pi invokes message_end handlers immediately before persisting the
|
|
142
|
+
// finalized user message. Advancing the leaf here makes this context-free
|
|
143
|
+
// marker the user's parent without writing anything during /tree itself.
|
|
144
|
+
pi.appendEntry<CodexThreadMarkerData>(CODEX_THREAD_MARKER_ENTRY_TYPE, {
|
|
145
|
+
version: 1,
|
|
146
|
+
sessionId,
|
|
147
|
+
threadId: uuidv7(),
|
|
148
|
+
forkedFromThreadId: parent.threadId,
|
|
149
|
+
branchParentEntryId,
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
154
|
+
pending.delete(ctx.sessionManager.getSessionId());
|
|
155
|
+
});
|
|
156
|
+
}
|