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.
Files changed (29) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +86 -38
  3. package/extensions/openai-codex-compat/apply-patch-diff-render.ts +6 -2
  4. package/extensions/openai-codex-compat/apply-patch-engine.ts +89 -5
  5. package/extensions/openai-codex-compat/apply-patch.ts +4 -5
  6. package/extensions/openai-codex-compat/codex-cache-diagnostics.ts +97 -0
  7. package/extensions/openai-codex-compat/codex-cache-key.ts +9 -0
  8. package/extensions/openai-codex-compat/codex-installation.ts +51 -0
  9. package/extensions/openai-codex-compat/codex-metadata.ts +139 -0
  10. package/extensions/openai-codex-compat/codex-protocol.ts +4 -2
  11. package/extensions/openai-codex-compat/codex-provider.ts +708 -128
  12. package/extensions/openai-codex-compat/codex-stream.ts +137 -40
  13. package/extensions/openai-codex-compat/codex-thread-lineage.ts +156 -0
  14. package/extensions/openai-codex-compat/codex-transport.ts +1795 -199
  15. package/extensions/openai-codex-compat/compaction-checkpoint.ts +2 -2
  16. package/extensions/openai-codex-compat/config.ts +15 -2
  17. package/extensions/openai-codex-compat/image-generation-schema.ts +37 -0
  18. package/extensions/openai-codex-compat/image-generation.ts +25 -48
  19. package/extensions/openai-codex-compat/index.ts +13 -0
  20. package/extensions/openai-codex-compat/namespaced-tools.ts +2 -0
  21. package/extensions/openai-codex-compat/output-limit-continuation.ts +151 -0
  22. package/extensions/openai-codex-compat/provider-error.ts +79 -0
  23. package/extensions/openai-codex-compat/remote-compaction.ts +13 -0
  24. package/extensions/openai-codex-compat/request-options.ts +2 -2
  25. package/extensions/openai-codex-compat/responses-lite.ts +147 -0
  26. package/extensions/openai-codex-compat/responses-replay.ts +0 -7
  27. package/extensions/openai-codex-compat/settings-pane.ts +11 -0
  28. package/extensions/openai-codex-compat/web-run.ts +7 -0
  29. 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: string;
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 }