pi-openai-codex-compat 0.0.2 → 0.0.3
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 +50 -0
- package/README.md +30 -16
- package/extensions/openai-codex-compat/apply-patch.ts +4 -5
- package/extensions/openai-codex-compat/codex-cache-key.ts +9 -0
- package/extensions/openai-codex-compat/codex-protocol.ts +3 -2
- package/extensions/openai-codex-compat/codex-provider.ts +221 -107
- package/extensions/openai-codex-compat/codex-stream.ts +85 -14
- package/extensions/openai-codex-compat/codex-transport.ts +714 -161
- package/extensions/openai-codex-compat/config.ts +2 -2
- package/extensions/openai-codex-compat/image-generation-schema.ts +37 -0
- package/extensions/openai-codex-compat/image-generation.ts +25 -48
- package/extensions/openai-codex-compat/index.ts +7 -0
- package/extensions/openai-codex-compat/provider-error.ts +79 -0
- package/extensions/openai-codex-compat/responses-replay.ts +0 -7
- package/extensions/openai-codex-compat/web-run.ts +7 -0
- package/package.json +2 -1
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type TextSignatureV1,
|
|
9
9
|
type ThinkingContent,
|
|
10
10
|
type ToolCall,
|
|
11
|
+
type Usage,
|
|
11
12
|
} from "@earendil-works/pi-ai";
|
|
12
13
|
import { isObject, type JsonRecord } from "./codex-protocol.ts";
|
|
13
14
|
import { CODEX_NAMESPACED_TOOL_NAMES, namespacedToolCallName } from "./namespaced-tools.ts";
|
|
@@ -38,6 +39,27 @@ type OutputSlot =
|
|
|
38
39
|
|
|
39
40
|
type ToolCallSlot = Extract<OutputSlot, { type: "toolCall" }>;
|
|
40
41
|
|
|
42
|
+
type ProcessCodexStreamOptions = {
|
|
43
|
+
applyServiceTierPricing?(usage: Usage, responseServiceTier: string | undefined): void;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type CodexResponseStatus =
|
|
47
|
+
| "completed"
|
|
48
|
+
| "incomplete"
|
|
49
|
+
| "failed"
|
|
50
|
+
| "cancelled"
|
|
51
|
+
| "queued"
|
|
52
|
+
| "in_progress";
|
|
53
|
+
|
|
54
|
+
const CODEX_RESPONSE_STATUSES = new Set<CodexResponseStatus>([
|
|
55
|
+
"completed",
|
|
56
|
+
"incomplete",
|
|
57
|
+
"failed",
|
|
58
|
+
"cancelled",
|
|
59
|
+
"queued",
|
|
60
|
+
"in_progress",
|
|
61
|
+
]);
|
|
62
|
+
|
|
41
63
|
function outputIndex(event: JsonRecord): number {
|
|
42
64
|
return typeof event["output_index"] === "number" ? event["output_index"] : 0;
|
|
43
65
|
}
|
|
@@ -60,7 +82,7 @@ function appendGrammarDelta(
|
|
|
60
82
|
): string | undefined {
|
|
61
83
|
if (buffer.closed) {
|
|
62
84
|
if (close && nextInput === buffer.input) return undefined;
|
|
63
|
-
throw new Error(`grammar tool input for property "${property}" changed after
|
|
85
|
+
throw new Error(`grammar tool input for property "${property}" changed after it was closed`);
|
|
64
86
|
}
|
|
65
87
|
if (!nextInput.startsWith(buffer.input)) {
|
|
66
88
|
throw new Error(`grammar tool input for property "${property}" changed non-monotonically`);
|
|
@@ -128,13 +150,35 @@ function reasoningText(item: JsonRecord): string {
|
|
|
128
150
|
.join("\n\n")
|
|
129
151
|
: "";
|
|
130
152
|
if (summary) return summary;
|
|
131
|
-
return
|
|
153
|
+
return Array.isArray(item.content)
|
|
154
|
+
? item.content
|
|
155
|
+
.filter(isObject)
|
|
156
|
+
.map((part) => (typeof part.text === "string" ? part.text : ""))
|
|
157
|
+
.join("\n\n")
|
|
158
|
+
: "";
|
|
132
159
|
}
|
|
133
160
|
|
|
134
|
-
function
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
161
|
+
function normalizeCodexStatus(status: unknown): CodexResponseStatus | undefined {
|
|
162
|
+
return typeof status === "string" && CODEX_RESPONSE_STATUSES.has(status as CodexResponseStatus)
|
|
163
|
+
? (status as CodexResponseStatus)
|
|
164
|
+
: undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function mapStopReason(
|
|
168
|
+
status: CodexResponseStatus | undefined,
|
|
169
|
+
incompleteReason: string | undefined,
|
|
170
|
+
): { stopReason: AssistantMessage["stopReason"]; errorMessage?: string } {
|
|
171
|
+
if (status === "incomplete") {
|
|
172
|
+
if (incompleteReason === "max_output_tokens") return { stopReason: "length" };
|
|
173
|
+
return {
|
|
174
|
+
stopReason: "error",
|
|
175
|
+
errorMessage: incompleteReason
|
|
176
|
+
? `Response incomplete: ${incompleteReason}`
|
|
177
|
+
: "Response incomplete without a provider reason",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (status === "failed" || status === "cancelled") return { stopReason: "error" };
|
|
181
|
+
return { stopReason: "stop" };
|
|
138
182
|
}
|
|
139
183
|
|
|
140
184
|
export async function processCodexStream(
|
|
@@ -143,10 +187,16 @@ export async function processCodexStream(
|
|
|
143
187
|
stream: AssistantMessageEventStream,
|
|
144
188
|
model: Model<any>,
|
|
145
189
|
grammarToolInputProperties: ReadonlyMap<string, string>,
|
|
190
|
+
options?: ProcessCodexStreamOptions,
|
|
146
191
|
): Promise<void> {
|
|
147
192
|
let terminal = false;
|
|
148
193
|
const slots = new Map<number, OutputSlot>();
|
|
149
194
|
const reasoningById = new Map<string, ThinkingContent>();
|
|
195
|
+
const applyMessagePhaseStopReason = (item: JsonRecord): void => {
|
|
196
|
+
if (item.type === "message" && item["phase"] === "final_answer") {
|
|
197
|
+
output.stopReason = "stop";
|
|
198
|
+
}
|
|
199
|
+
};
|
|
150
200
|
|
|
151
201
|
const getSlot = <TType extends OutputSlot["type"]>(
|
|
152
202
|
index: number,
|
|
@@ -180,6 +230,7 @@ export async function processCodexStream(
|
|
|
180
230
|
return slot;
|
|
181
231
|
}
|
|
182
232
|
if (item.type === "message") {
|
|
233
|
+
applyMessagePhaseStopReason(item);
|
|
183
234
|
const block: TextContent = { type: "text", text: "" };
|
|
184
235
|
output.content.push(block);
|
|
185
236
|
const slot = {
|
|
@@ -270,12 +321,15 @@ export async function processCodexStream(
|
|
|
270
321
|
typeof outputDetails?.["reasoning_tokens"] === "number"
|
|
271
322
|
? outputDetails["reasoning_tokens"]
|
|
272
323
|
: 0,
|
|
273
|
-
totalTokens:
|
|
274
|
-
typeof usage.total_tokens === "number" ? usage.total_tokens : input + outputTokens,
|
|
324
|
+
totalTokens: typeof usage.total_tokens === "number" ? usage.total_tokens || 0 : 0,
|
|
275
325
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
276
326
|
};
|
|
277
|
-
calculateCost(model, output.usage);
|
|
278
327
|
}
|
|
328
|
+
calculateCost(model, output.usage);
|
|
329
|
+
options?.applyServiceTierPricing?.(
|
|
330
|
+
output.usage,
|
|
331
|
+
typeof response.service_tier === "string" ? response.service_tier : undefined,
|
|
332
|
+
);
|
|
279
333
|
for (const item of responseItems(response["output"])) {
|
|
280
334
|
if (item.type !== "reasoning" || typeof item.id !== "string") continue;
|
|
281
335
|
const block = reasoningById.get(item.id);
|
|
@@ -288,8 +342,20 @@ export async function processCodexStream(
|
|
|
288
342
|
});
|
|
289
343
|
}
|
|
290
344
|
}
|
|
291
|
-
|
|
292
|
-
|
|
345
|
+
const status = normalizeCodexStatus(response["status"]);
|
|
346
|
+
const incompleteDetails = isObject(response["incomplete_details"])
|
|
347
|
+
? response["incomplete_details"]
|
|
348
|
+
: undefined;
|
|
349
|
+
const incompleteReason =
|
|
350
|
+
typeof incompleteDetails?.["reason"] === "string" ? incompleteDetails["reason"] : undefined;
|
|
351
|
+
const rawStopReason =
|
|
352
|
+
status === "incomplete" && incompleteReason ? `${status}.${incompleteReason}` : status;
|
|
353
|
+
if (rawStopReason === undefined) delete output.rawStopReason;
|
|
354
|
+
else output.rawStopReason = rawStopReason;
|
|
355
|
+
const mappedStop = mapStopReason(status, incompleteReason);
|
|
356
|
+
output.stopReason = mappedStop.stopReason;
|
|
357
|
+
if (mappedStop.errorMessage === undefined) delete output.errorMessage;
|
|
358
|
+
else output.errorMessage = mappedStop.errorMessage;
|
|
293
359
|
if (output.stopReason === "stop" && output.content.some((block) => block.type === "toolCall")) {
|
|
294
360
|
output.stopReason = "toolUse";
|
|
295
361
|
}
|
|
@@ -353,8 +419,10 @@ export async function processCodexStream(
|
|
|
353
419
|
const previous = slot.block.partialJson;
|
|
354
420
|
slot.block.partialJson = event.arguments;
|
|
355
421
|
slot.block.arguments = parseStreamingJson(event.arguments);
|
|
356
|
-
if (event.arguments.startsWith(previous))
|
|
357
|
-
|
|
422
|
+
if (event.arguments.startsWith(previous)) {
|
|
423
|
+
const delta = event.arguments.slice(previous.length);
|
|
424
|
+
if (delta.length > 0) pushToolDelta(slot, delta);
|
|
425
|
+
}
|
|
358
426
|
} else if (event.type === "response.custom_tool_call_input.delta") {
|
|
359
427
|
const slot = getSlot(index, "toolCall");
|
|
360
428
|
if (!slot || typeof event["delta"] !== "string") continue;
|
|
@@ -368,6 +436,7 @@ export async function processCodexStream(
|
|
|
368
436
|
pushToolDelta(slot, appendCustomInput(slot.block, event["input"], true));
|
|
369
437
|
} else if (event.type === "response.output_item.done" && isObject(event.item)) {
|
|
370
438
|
const item = event.item;
|
|
439
|
+
applyMessagePhaseStopReason(item);
|
|
371
440
|
const slot = slotFor(index, item);
|
|
372
441
|
if (item.type === "reasoning" && slot?.type === "thinking") {
|
|
373
442
|
slot.block.thinking = reasoningText(item) || slot.block.thinking;
|
|
@@ -440,5 +509,7 @@ export async function processCodexStream(
|
|
|
440
509
|
}
|
|
441
510
|
}
|
|
442
511
|
|
|
443
|
-
if (!terminal)
|
|
512
|
+
if (!terminal) {
|
|
513
|
+
throw new Error("OpenAI Responses stream ended before a terminal response event");
|
|
514
|
+
}
|
|
444
515
|
}
|