pi-openai-codex-compat 0.0.2 → 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 +86 -0
- package/README.md +86 -38
- 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/apply-patch.ts +4 -5
- package/extensions/openai-codex-compat/codex-cache-diagnostics.ts +97 -0
- package/extensions/openai-codex-compat/codex-cache-key.ts +9 -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 +4 -2
- package/extensions/openai-codex-compat/codex-provider.ts +708 -128
- package/extensions/openai-codex-compat/codex-stream.ts +137 -40
- package/extensions/openai-codex-compat/codex-thread-lineage.ts +156 -0
- package/extensions/openai-codex-compat/codex-transport.ts +1795 -199
- package/extensions/openai-codex-compat/compaction-checkpoint.ts +2 -2
- package/extensions/openai-codex-compat/config.ts +15 -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 +13 -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/provider-error.ts +79 -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/responses-replay.ts +0 -7
- package/extensions/openai-codex-compat/settings-pane.ts +11 -0
- package/extensions/openai-codex-compat/web-run.ts +7 -0
- package/package.json +2 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { type JsonRecord } from "./codex-protocol.ts";
|
|
3
|
+
import { usesResponsesLite } from "./responses-lite.ts";
|
|
4
|
+
|
|
5
|
+
type Fingerprint = {
|
|
6
|
+
bytes: number;
|
|
7
|
+
sha256: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type CodexCacheDiagnosticContext = {
|
|
11
|
+
envelope: "responses" | "responses_lite";
|
|
12
|
+
prewarmMode: "static";
|
|
13
|
+
fullInputItems: number;
|
|
14
|
+
staticInputItems: number;
|
|
15
|
+
staticPrefixBytes: number;
|
|
16
|
+
staticPrefixSha256: string;
|
|
17
|
+
staticRequestBytes: number;
|
|
18
|
+
staticRequestSha256: string;
|
|
19
|
+
instructionsBytes?: number;
|
|
20
|
+
instructionsSha256?: string;
|
|
21
|
+
toolsBytes?: number;
|
|
22
|
+
toolsSha256?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function jsonFingerprint(value: unknown): Fingerprint {
|
|
26
|
+
const json = JSON.stringify(value);
|
|
27
|
+
return {
|
|
28
|
+
bytes: Buffer.byteLength(json, "utf8"),
|
|
29
|
+
sha256: createHash("sha256").update(json, "utf8").digest("hex"),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function textFingerprint(value: string): Fingerprint {
|
|
34
|
+
return {
|
|
35
|
+
bytes: Buffer.byteLength(value, "utf8"),
|
|
36
|
+
sha256: createHash("sha256").update(value, "utf8").digest("hex"),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function inputLength(payload: JsonRecord): number {
|
|
41
|
+
return Array.isArray(payload.input) ? payload.input.length : 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function staticRequest(payload: JsonRecord): JsonRecord {
|
|
45
|
+
const result = structuredClone(payload);
|
|
46
|
+
delete result.client_metadata;
|
|
47
|
+
delete result.prompt_cache_key;
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function codexCacheDiagnosticContext(
|
|
52
|
+
ordinaryBody: JsonRecord,
|
|
53
|
+
fullWireBody: JsonRecord,
|
|
54
|
+
staticWireBody: JsonRecord,
|
|
55
|
+
modelId: string,
|
|
56
|
+
responsesLiteEnabled = true,
|
|
57
|
+
): CodexCacheDiagnosticContext {
|
|
58
|
+
const lite = usesResponsesLite(modelId, responsesLiteEnabled);
|
|
59
|
+
const staticPrefix = lite
|
|
60
|
+
? (staticWireBody.input ?? [])
|
|
61
|
+
: {
|
|
62
|
+
...("instructions" in ordinaryBody ? { instructions: ordinaryBody.instructions } : {}),
|
|
63
|
+
...("tools" in ordinaryBody ? { tools: ordinaryBody.tools } : {}),
|
|
64
|
+
};
|
|
65
|
+
const prefixFingerprint = jsonFingerprint(staticPrefix);
|
|
66
|
+
const requestFingerprint = jsonFingerprint(staticRequest(staticWireBody));
|
|
67
|
+
const instructionFingerprint =
|
|
68
|
+
typeof ordinaryBody.instructions === "string"
|
|
69
|
+
? textFingerprint(ordinaryBody.instructions)
|
|
70
|
+
: undefined;
|
|
71
|
+
const toolsFingerprint = Array.isArray(ordinaryBody.tools)
|
|
72
|
+
? jsonFingerprint(ordinaryBody.tools)
|
|
73
|
+
: undefined;
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
envelope: lite ? "responses_lite" : "responses",
|
|
77
|
+
prewarmMode: "static",
|
|
78
|
+
fullInputItems: inputLength(fullWireBody),
|
|
79
|
+
staticInputItems: inputLength(staticWireBody),
|
|
80
|
+
staticPrefixBytes: prefixFingerprint.bytes,
|
|
81
|
+
staticPrefixSha256: prefixFingerprint.sha256,
|
|
82
|
+
staticRequestBytes: requestFingerprint.bytes,
|
|
83
|
+
staticRequestSha256: requestFingerprint.sha256,
|
|
84
|
+
...(instructionFingerprint
|
|
85
|
+
? {
|
|
86
|
+
instructionsBytes: instructionFingerprint.bytes,
|
|
87
|
+
instructionsSha256: instructionFingerprint.sha256,
|
|
88
|
+
}
|
|
89
|
+
: {}),
|
|
90
|
+
...(toolsFingerprint
|
|
91
|
+
? {
|
|
92
|
+
toolsBytes: toolsFingerprint.bytes,
|
|
93
|
+
toolsSha256: toolsFingerprint.sha256,
|
|
94
|
+
}
|
|
95
|
+
: {}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH = 64;
|
|
4
|
+
|
|
5
|
+
export function codexCacheKey(sessionId: string | undefined): string | undefined {
|
|
6
|
+
if (sessionId === undefined) return undefined;
|
|
7
|
+
if (Array.from(sessionId).length <= OPENAI_PROMPT_CACHE_KEY_MAX_LENGTH) return sessionId;
|
|
8
|
+
return createHash("sha256").update(sessionId, "utf8").digest("hex");
|
|
9
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
export const CODEX_INSTALLATION_ID_FILE = "openai-codex-compat-installation-id";
|
|
7
|
+
|
|
8
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
9
|
+
|
|
10
|
+
function readInstallationId(path: string): string {
|
|
11
|
+
const value = readFileSync(path, "utf8").trim();
|
|
12
|
+
if (!UUID_PATTERN.test(value)) {
|
|
13
|
+
throw new Error(`Invalid Codex installation id in ${path}`);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveCodexInstallationId(agentDir = getAgentDir()): string {
|
|
19
|
+
const path = join(agentDir, CODEX_INSTALLATION_ID_FILE);
|
|
20
|
+
try {
|
|
21
|
+
return readInstallationId(path);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (
|
|
24
|
+
!(error instanceof Error) ||
|
|
25
|
+
!("code" in error) ||
|
|
26
|
+
(error as NodeJS.ErrnoException).code !== "ENOENT"
|
|
27
|
+
) {
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
mkdirSync(agentDir, { recursive: true });
|
|
33
|
+
const installationId = randomUUID();
|
|
34
|
+
try {
|
|
35
|
+
writeFileSync(path, `${installationId}\n`, {
|
|
36
|
+
encoding: "utf8",
|
|
37
|
+
flag: "wx",
|
|
38
|
+
mode: 0o644,
|
|
39
|
+
});
|
|
40
|
+
return installationId;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (
|
|
43
|
+
error instanceof Error &&
|
|
44
|
+
"code" in error &&
|
|
45
|
+
(error as NodeJS.ErrnoException).code === "EEXIST"
|
|
46
|
+
) {
|
|
47
|
+
return readInstallationId(path);
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { uuidv7 } from "@earendil-works/pi-ai";
|
|
2
|
+
import { isObject, type JsonRecord } from "./codex-protocol.ts";
|
|
3
|
+
|
|
4
|
+
export const CODEX_TURN_METADATA_HEADER = "x-codex-turn-metadata";
|
|
5
|
+
export const CODEX_INSTALLATION_ID_METADATA_KEY = "x-codex-installation-id";
|
|
6
|
+
export const CODEX_WINDOW_ID_HEADER = "x-codex-window-id";
|
|
7
|
+
|
|
8
|
+
export type CodexRequestKind = "turn" | "prewarm" | "compaction";
|
|
9
|
+
|
|
10
|
+
export type CodexCompactionMetadata = {
|
|
11
|
+
trigger: "manual" | "auto";
|
|
12
|
+
reason: "user_requested" | "context_limit" | "model_downshift" | "comp_hash_changed";
|
|
13
|
+
implementation: "responses" | "responses_compaction_v2" | "responses_compact";
|
|
14
|
+
phase: "standalone_turn" | "pre_turn" | "mid_turn";
|
|
15
|
+
strategy: "memento" | "prefix_compaction";
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type CodexMetadataRequest =
|
|
19
|
+
| { kind: "turn" | "prewarm" }
|
|
20
|
+
| { kind: "compaction"; compaction: CodexCompactionMetadata };
|
|
21
|
+
|
|
22
|
+
export type CodexMetadataIdentity = {
|
|
23
|
+
installationId: string;
|
|
24
|
+
threadId?: string;
|
|
25
|
+
forkedFromThreadId?: string;
|
|
26
|
+
windowNumber?: number;
|
|
27
|
+
turnStartedAtUnixMs?: number;
|
|
28
|
+
threadSource?: string;
|
|
29
|
+
sandbox?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const EPHEMERAL_METADATA_IDENTITY: CodexMetadataIdentity = { installationId: uuidv7() };
|
|
33
|
+
|
|
34
|
+
type CodexClientMetadata = {
|
|
35
|
+
[CODEX_INSTALLATION_ID_METADATA_KEY]: string;
|
|
36
|
+
session_id: string;
|
|
37
|
+
thread_id: string;
|
|
38
|
+
turn_id: string;
|
|
39
|
+
[CODEX_WINDOW_ID_HEADER]: string;
|
|
40
|
+
[CODEX_TURN_METADATA_HEADER]: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function responsesCompactionV2Metadata(
|
|
44
|
+
trigger: CodexCompactionMetadata["trigger"],
|
|
45
|
+
reason: CodexCompactionMetadata["reason"],
|
|
46
|
+
phase: CodexCompactionMetadata["phase"],
|
|
47
|
+
): CodexCompactionMetadata {
|
|
48
|
+
return {
|
|
49
|
+
trigger,
|
|
50
|
+
reason,
|
|
51
|
+
implementation: "responses_compaction_v2",
|
|
52
|
+
phase,
|
|
53
|
+
strategy: "memento",
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function windowId(sessionId: string, identity: CodexMetadataIdentity): string {
|
|
58
|
+
return `${identity.threadId ?? sessionId}:${identity.windowNumber ?? 0}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function turnMetadata(
|
|
62
|
+
sessionId: string,
|
|
63
|
+
turnId: string,
|
|
64
|
+
request: CodexMetadataRequest,
|
|
65
|
+
identity: CodexMetadataIdentity,
|
|
66
|
+
): string {
|
|
67
|
+
const threadId = identity.threadId ?? sessionId;
|
|
68
|
+
return JSON.stringify({
|
|
69
|
+
installation_id: identity.installationId,
|
|
70
|
+
session_id: sessionId,
|
|
71
|
+
thread_id: threadId,
|
|
72
|
+
turn_id: turnId,
|
|
73
|
+
window_id: windowId(sessionId, identity),
|
|
74
|
+
request_kind: request.kind,
|
|
75
|
+
...(identity.forkedFromThreadId ? { forked_from_thread_id: identity.forkedFromThreadId } : {}),
|
|
76
|
+
...(identity.threadSource ? { thread_source: identity.threadSource } : {}),
|
|
77
|
+
...(identity.sandbox ? { sandbox: identity.sandbox } : {}),
|
|
78
|
+
...(identity.turnStartedAtUnixMs === undefined
|
|
79
|
+
? {}
|
|
80
|
+
: { turn_started_at_unix_ms: identity.turnStartedAtUnixMs }),
|
|
81
|
+
...(request.kind === "compaction" ? { compaction: request.compaction } : {}),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function codexClientMetadata(
|
|
86
|
+
sessionId: string | undefined,
|
|
87
|
+
request: CodexMetadataRequest,
|
|
88
|
+
turnId = uuidv7(),
|
|
89
|
+
identity: CodexMetadataIdentity = EPHEMERAL_METADATA_IDENTITY,
|
|
90
|
+
): CodexClientMetadata | undefined {
|
|
91
|
+
if (!sessionId) return undefined;
|
|
92
|
+
const threadId = identity.threadId ?? sessionId;
|
|
93
|
+
return {
|
|
94
|
+
[CODEX_INSTALLATION_ID_METADATA_KEY]: identity.installationId,
|
|
95
|
+
session_id: sessionId,
|
|
96
|
+
thread_id: threadId,
|
|
97
|
+
turn_id: turnId,
|
|
98
|
+
[CODEX_WINDOW_ID_HEADER]: windowId(sessionId, identity),
|
|
99
|
+
[CODEX_TURN_METADATA_HEADER]: turnMetadata(sessionId, turnId, request, identity),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function withCodexRequestMetadata(
|
|
104
|
+
payload: JsonRecord,
|
|
105
|
+
sessionId: string | undefined,
|
|
106
|
+
request: CodexMetadataRequest,
|
|
107
|
+
turnId?: string,
|
|
108
|
+
identity: CodexMetadataIdentity = EPHEMERAL_METADATA_IDENTITY,
|
|
109
|
+
): JsonRecord {
|
|
110
|
+
const metadata = codexClientMetadata(sessionId, request, turnId, identity);
|
|
111
|
+
if (!metadata) {
|
|
112
|
+
const result = structuredClone(payload);
|
|
113
|
+
delete result["client_metadata"];
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
const existing = isObject(payload["client_metadata"]) ? payload["client_metadata"] : {};
|
|
117
|
+
return {
|
|
118
|
+
...payload,
|
|
119
|
+
client_metadata: {
|
|
120
|
+
...existing,
|
|
121
|
+
...metadata,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function applyCodexMetadataHeaders(headers: Headers, payload: JsonRecord): void {
|
|
127
|
+
const metadata = payload["client_metadata"];
|
|
128
|
+
if (!isObject(metadata)) return;
|
|
129
|
+
|
|
130
|
+
if (typeof metadata["thread_id"] === "string") {
|
|
131
|
+
headers.set("thread-id", metadata["thread_id"]);
|
|
132
|
+
}
|
|
133
|
+
if (typeof metadata[CODEX_WINDOW_ID_HEADER] === "string") {
|
|
134
|
+
headers.set(CODEX_WINDOW_ID_HEADER, metadata[CODEX_WINDOW_ID_HEADER]);
|
|
135
|
+
}
|
|
136
|
+
if (typeof metadata[CODEX_TURN_METADATA_HEADER] === "string") {
|
|
137
|
+
headers.set(CODEX_TURN_METADATA_HEADER, metadata[CODEX_TURN_METADATA_HEADER]);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -28,6 +28,7 @@ export interface JsonRecord {
|
|
|
28
28
|
prompt_cache_key?: unknown;
|
|
29
29
|
tools?: unknown;
|
|
30
30
|
service_tier?: unknown;
|
|
31
|
+
client_metadata?: unknown;
|
|
31
32
|
chatgpt_account_id?: unknown;
|
|
32
33
|
message?: unknown;
|
|
33
34
|
item?: unknown;
|
|
@@ -269,7 +270,7 @@ export function remoteCompactionPayload(options: {
|
|
|
269
270
|
modelId: string;
|
|
270
271
|
history: readonly ResponsesItem[];
|
|
271
272
|
instructions: string;
|
|
272
|
-
sessionId
|
|
273
|
+
sessionId?: string | undefined;
|
|
273
274
|
fallbackTools?: unknown[] | undefined;
|
|
274
275
|
priority: boolean;
|
|
275
276
|
}): JsonRecord {
|
|
@@ -290,7 +291,8 @@ export function remoteCompactionPayload(options: {
|
|
|
290
291
|
typeof payload.parallel_tool_calls === "boolean" ? payload.parallel_tool_calls : true;
|
|
291
292
|
payload.tool_choice ??= "auto";
|
|
292
293
|
payload.include = [...new Set([...include, "reasoning.encrypted_content"])];
|
|
293
|
-
payload.prompt_cache_key = options.sessionId;
|
|
294
|
+
if (options.sessionId) payload.prompt_cache_key = options.sessionId;
|
|
295
|
+
else delete payload.prompt_cache_key;
|
|
294
296
|
payload.text =
|
|
295
297
|
isObject(payload.text) && typeof payload.text.verbosity === "string"
|
|
296
298
|
? { verbosity: payload.text.verbosity }
|