@oai404iao/pi-codex-core 0.1.0-alpha.1

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 (53) hide show
  1. package/LICENSE +28 -0
  2. package/LICENSES/Apache-2.0.txt +201 -0
  3. package/LICENSES/OpenAI-Codex-NOTICE.txt +6 -0
  4. package/README.md +26 -0
  5. package/THIRD_PARTY_NOTICES.md +18 -0
  6. package/package.json +84 -0
  7. package/provenance/openai-codex-eb9dceba-reserved-tools.json +140 -0
  8. package/src/adapter/compaction/checkpoint.ts +159 -0
  9. package/src/adapter/compaction/collect.ts +51 -0
  10. package/src/adapter/compaction/http.ts +101 -0
  11. package/src/adapter/compaction/request.ts +159 -0
  12. package/src/adapter/compaction/transport.ts +125 -0
  13. package/src/adapter/compaction/websocket.ts +119 -0
  14. package/src/extension/prewarm-snapshot.ts +27 -0
  15. package/src/extension/provider-runtime.ts +101 -0
  16. package/src/extension/startup-prewarm.ts +264 -0
  17. package/src/fast-mode.ts +124 -0
  18. package/src/index.ts +257 -0
  19. package/src/native-compaction.ts +392 -0
  20. package/src/patch/apply.ts +338 -0
  21. package/src/patch/parser.ts +224 -0
  22. package/src/patch/render.ts +201 -0
  23. package/src/provider-native-tools.ts +75 -0
  24. package/src/providers/codex-apply-patch-tool.ts +23 -0
  25. package/src/providers/codex-apply-patch.lark +19 -0
  26. package/src/providers/openai-codex/cache-key.ts +52 -0
  27. package/src/providers/openai-codex/captured-stream.ts +50 -0
  28. package/src/providers/openai-codex/constants.ts +61 -0
  29. package/src/providers/openai-codex/continuation.ts +110 -0
  30. package/src/providers/openai-codex/errors.ts +130 -0
  31. package/src/providers/openai-codex/events.ts +123 -0
  32. package/src/providers/openai-codex/headers.ts +224 -0
  33. package/src/providers/openai-codex/lite.ts +24 -0
  34. package/src/providers/openai-codex/message.ts +33 -0
  35. package/src/providers/openai-codex/prewarm.ts +76 -0
  36. package/src/providers/openai-codex/proxy.ts +55 -0
  37. package/src/providers/openai-codex/reasoning.ts +54 -0
  38. package/src/providers/openai-codex/request-body.ts +149 -0
  39. package/src/providers/openai-codex/request-context.ts +20 -0
  40. package/src/providers/openai-codex/request-metadata.ts +137 -0
  41. package/src/providers/openai-codex/retry.ts +154 -0
  42. package/src/providers/openai-codex/runtime.ts +1 -0
  43. package/src/providers/openai-codex/sse.ts +93 -0
  44. package/src/providers/openai-codex/stream.ts +367 -0
  45. package/src/providers/openai-codex/urls.ts +24 -0
  46. package/src/providers/openai-codex/usage.ts +60 -0
  47. package/src/providers/openai-codex/websocket-connection.ts +216 -0
  48. package/src/providers/openai-codex/websocket-events.ts +210 -0
  49. package/src/providers/openai-codex/websocket-session.ts +192 -0
  50. package/src/providers/openai-codex/websocket-socket.ts +18 -0
  51. package/src/providers/openai-codex/websocket-stream.ts +151 -0
  52. package/src/tools/apply-patch.ts +84 -0
  53. package/src/tools/view-image.ts +98 -0
@@ -0,0 +1,201 @@
1
+ import { Box, Container, Spacer, Text, type Component } from "@earendil-works/pi-tui";
2
+ import { previewApplyPatch, type ApplyPatchPreviewFile } from "./apply.js";
3
+
4
+ const PREVIEW_INTERVAL_MS = 500;
5
+
6
+ type RenderPreview = { complete: boolean; files: ApplyPatchPreviewFile[] } | { error: string };
7
+
8
+ interface PreviewRequest {
9
+ input: string;
10
+ cwd: string;
11
+ complete: boolean;
12
+ invalidate: () => void;
13
+ }
14
+
15
+ interface ApplyPatchRenderBox extends Box {
16
+ preview?: RenderPreview;
17
+ request?: PreviewRequest;
18
+ requestKey?: string;
19
+ previewPending: boolean;
20
+ previewTimer?: ReturnType<typeof setTimeout>;
21
+ lastPreviewStartedAt: number;
22
+ executionStarted: boolean;
23
+ settledFiles?: Array<{ kind: ApplyPatchPreviewFile["kind"]; path: string; moveTo?: string }>;
24
+ settledError: boolean;
25
+ settledSuccess: boolean;
26
+ }
27
+
28
+ async function computePreview(request: PreviewRequest): Promise<RenderPreview> {
29
+ try {
30
+ const preview = await previewApplyPatch(request.input, {
31
+ cwd: request.cwd,
32
+ }, request.complete);
33
+ return {
34
+ complete: preview.complete,
35
+ files: preview.files,
36
+ };
37
+ } catch (error) {
38
+ return { error: error instanceof Error ? error.message : String(error) };
39
+ }
40
+ }
41
+
42
+ function createRenderBox(): ApplyPatchRenderBox {
43
+ return Object.assign(new Box(1, 1, (text: string) => text), {
44
+ previewPending: false,
45
+ lastPreviewStartedAt: 0,
46
+ executionStarted: false,
47
+ settledError: false,
48
+ settledSuccess: false,
49
+ });
50
+ }
51
+
52
+ function getRenderBox(state: Record<string, unknown>, lastComponent: Component | undefined): ApplyPatchRenderBox {
53
+ if (lastComponent instanceof Box) {
54
+ state.callComponent = lastComponent;
55
+ return lastComponent as ApplyPatchRenderBox;
56
+ }
57
+ if (state.callComponent instanceof Box) return state.callComponent as ApplyPatchRenderBox;
58
+ const component = createRenderBox();
59
+ state.callComponent = component;
60
+ return component;
61
+ }
62
+
63
+ function requestKey(request: Omit<PreviewRequest, "invalidate">): string {
64
+ return `${request.complete ? "complete" : "partial"}\u0000${request.cwd}\u0000${request.input}`;
65
+ }
66
+
67
+ function schedulePreview(component: ApplyPatchRenderBox, immediate = false): void {
68
+ if (!component.request || component.previewPending) return;
69
+ if (component.previewTimer) {
70
+ if (!immediate) return;
71
+ clearTimeout(component.previewTimer);
72
+ component.previewTimer = undefined;
73
+ }
74
+ const delay = immediate ? 0 : Math.max(0, PREVIEW_INTERVAL_MS - (Date.now() - component.lastPreviewStartedAt));
75
+ component.previewTimer = setTimeout(() => {
76
+ component.previewTimer = undefined;
77
+ const request = component.request;
78
+ if (!request) return;
79
+ const key = component.requestKey;
80
+ component.previewPending = true;
81
+ component.lastPreviewStartedAt = Date.now();
82
+ void computePreview(request).then((preview) => {
83
+ if (!component.executionStarted && component.requestKey === key) {
84
+ component.preview = preview;
85
+ }
86
+ }).finally(() => {
87
+ component.previewPending = false;
88
+ request.invalidate();
89
+ if (!component.executionStarted && component.requestKey !== key) schedulePreview(component, Boolean(component.request?.complete));
90
+ });
91
+ }, delay);
92
+ component.previewTimer.unref?.();
93
+ }
94
+
95
+ function updatePreviewRequest(component: ApplyPatchRenderBox, request: PreviewRequest): void {
96
+ const key = requestKey(request);
97
+ if (component.requestKey === key) return;
98
+ component.request = request;
99
+ component.requestKey = key;
100
+ schedulePreview(component, request.complete);
101
+ }
102
+
103
+ function contentLineCount(content: string): number {
104
+ if (!content) return 0;
105
+ const lines = content.replace(/\r\n/g, "\n").split("\n");
106
+ if (lines[lines.length - 1] === "") lines.pop();
107
+ return lines.length;
108
+ }
109
+
110
+ function operationLine(file: ApplyPatchPreviewFile, theme: any, expanded: boolean): string {
111
+ const marker = file.kind === "add" ? "A" : file.kind === "delete" ? "D" : "M";
112
+ const markerColor = file.kind === "delete" ? "error" : file.kind === "add" ? "success" : "accent";
113
+ const path = file.moveTo ? `${file.path} → ${file.moveTo}` : file.path;
114
+ const before = contentLineCount(file.previousContent);
115
+ const after = contentLineCount(file.content);
116
+ const size = file.kind === "add" ? `+${after} lines` : file.kind === "delete" ? `-${before} lines` : `${before}→${after} lines`;
117
+ const detail = expanded && file.moveTo ? ` (${file.path} moved)` : "";
118
+ return `${theme.fg(markerColor, marker)} ${theme.fg("accent", path)} ${theme.fg("dim", `${size}${detail}`)}`;
119
+ }
120
+
121
+ function settledOperationLine(file: { kind: ApplyPatchPreviewFile["kind"]; path: string; moveTo?: string }, theme: any): string {
122
+ const marker = file.kind === "add" ? "A" : file.kind === "delete" ? "D" : "M";
123
+ const markerColor = file.kind === "delete" ? "error" : file.kind === "add" ? "success" : "accent";
124
+ const path = file.moveTo ? `${file.path} → ${file.moveTo}` : file.path;
125
+ return `${theme.fg(markerColor, marker)} ${theme.fg("accent", path)}`;
126
+ }
127
+
128
+ function rebuildCall(component: ApplyPatchRenderBox, theme: any, expanded: boolean): ApplyPatchRenderBox {
129
+ component.setBgFn(component.settledError
130
+ ? (text: string) => theme.bg("toolErrorBg", text)
131
+ : component.settledSuccess
132
+ ? (text: string) => theme.bg("toolSuccessBg", text)
133
+ : (text: string) => theme.bg("toolPendingBg", text));
134
+ component.clear();
135
+ const status = component.settledError
136
+ ? "failed"
137
+ : component.settledSuccess
138
+ ? "applied"
139
+ : component.executionStarted
140
+ ? "applying"
141
+ : component.previewPending
142
+ ? "previewing"
143
+ : component.preview && "complete" in component.preview && component.preview.complete ? "ready" : "streaming";
144
+ component.addChild(new Text(`${theme.fg("toolTitle", theme.bold("apply_patch"))} ${theme.fg("dim", status)}`, 0, 0));
145
+ if (component.settledFiles) {
146
+ for (const file of component.settledFiles) component.addChild(new Text(settledOperationLine(file, theme), 0, 0));
147
+ return component;
148
+ }
149
+ if (!component.preview) return component;
150
+ if ("error" in component.preview) {
151
+ component.addChild(new Spacer(1));
152
+ component.addChild(new Text(theme.fg("error", component.preview.error), 0, 0));
153
+ return component;
154
+ }
155
+ for (const file of component.preview.files) component.addChild(new Text(operationLine(file, theme, expanded), 0, 0));
156
+ return component;
157
+ }
158
+
159
+ export function createApplyPatchRenderers() {
160
+ return {
161
+ renderCall(args: { input?: string } | undefined, theme: any, context: any) {
162
+ const component = getRenderBox(context.state, context.lastComponent);
163
+ if (context.executionStarted && !component.executionStarted) {
164
+ component.executionStarted = true;
165
+ if (component.previewTimer) clearTimeout(component.previewTimer);
166
+ component.previewTimer = undefined;
167
+ component.request = undefined;
168
+ component.requestKey = undefined;
169
+ }
170
+ const input = typeof args?.input === "string" ? args.input : "";
171
+ if (input && !component.executionStarted) {
172
+ updatePreviewRequest(component, {
173
+ input,
174
+ cwd: context.cwd,
175
+ complete: Boolean(context.argsComplete),
176
+ invalidate: context.invalidate,
177
+ });
178
+ }
179
+ return rebuildCall(component, theme, Boolean(context.expanded));
180
+ },
181
+ renderResult(result: {
182
+ content?: Array<{ type?: string; text?: string }>;
183
+ details?: { files?: Array<{ kind?: string; path?: string; moveTo?: string }> };
184
+ }, _options: unknown, theme: any, context: any) {
185
+ const component = getRenderBox(context.state, context.state.callComponent);
186
+ component.settledError = Boolean(context.isError);
187
+ component.settledSuccess = !context.isError;
188
+ if (!context.isError && Array.isArray(result.details?.files)) {
189
+ component.settledFiles = result.details.files.flatMap((file) =>
190
+ (file.kind === "add" || file.kind === "update" || file.kind === "delete") && typeof file.path === "string"
191
+ ? [{ kind: file.kind, path: file.path, ...(typeof file.moveTo === "string" ? { moveTo: file.moveTo } : {}) }]
192
+ : []);
193
+ component.preview = undefined;
194
+ }
195
+ rebuildCall(component, theme, Boolean(context.expanded));
196
+ const message = result.content?.filter((item) => item.type === "text").map((item) => item.text ?? "").join("\n") ?? "";
197
+ if (context.isError) return message ? new Text(theme.fg("error", message), 0, 0) : new Container();
198
+ return new Text(theme.fg("success", context.expanded && message ? message : "Applied"), 0, 0);
199
+ },
200
+ };
201
+ }
@@ -0,0 +1,75 @@
1
+ import { createCodexReservedNamespaceTool } from "@oai404iao/pi-codex-runtime/internal/codex-reserved-tools";
2
+ import type { NativeToolOwnership } from "@oai404iao/pi-codex-runtime/internal/providers/openai-codex/types";
3
+
4
+ export interface NativeToolRewriteResult<T = unknown> {
5
+ payload: T;
6
+ rewritten: string[];
7
+ }
8
+
9
+ export interface NativeToolRewriteOptions {
10
+ ownsNativeTool?: NativeToolOwnership;
11
+ imageModel?: string;
12
+ imageGeneration?: boolean | "hosted" | "standalone";
13
+ webSearch?: boolean | {
14
+ implementation?: "hosted" | "standalone";
15
+ contentTypes?: readonly ("text" | "image")[];
16
+ };
17
+ }
18
+
19
+ function isRecord(value: unknown): value is Record<string, unknown> {
20
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
21
+ }
22
+
23
+ function toolName(tool: Record<string, unknown>): string | undefined {
24
+ if (typeof tool.name === "string") return tool.name;
25
+ const nested = isRecord(tool.function) ? tool.function : undefined;
26
+ return typeof nested?.name === "string" ? nested.name : undefined;
27
+ }
28
+
29
+ function imageToolConfig(tool: Record<string, unknown>, options: NativeToolRewriteOptions): Record<string, unknown> {
30
+ const parameters = isRecord(tool.parameters) ? tool.parameters : isRecord(isRecord(tool.function) ? tool.function.parameters : undefined) ? (tool.function as Record<string, unknown>).parameters as Record<string, unknown> : {};
31
+ const config: Record<string, unknown> = { type: "image_generation" };
32
+ if (typeof options.imageModel === "string" && options.imageModel.trim()) config.model = options.imageModel.trim();
33
+ for (const key of ["size", "quality", "background", "output_format"]) {
34
+ const value = parameters[key];
35
+ if (typeof value === "string") config[key] = value;
36
+ }
37
+ if (!config.output_format) config.output_format = "png";
38
+ if (!config.action) config.action = "auto";
39
+ return config;
40
+ }
41
+
42
+ export function rewriteNativeOpenAiTools<T>(payload: T, options: NativeToolRewriteOptions = {}): NativeToolRewriteResult<T> {
43
+ if (!isRecord(payload) || !Array.isArray(payload.tools)) return { payload, rewritten: [] };
44
+ const rewritten: string[] = [];
45
+ const tools = payload.tools.map((candidate) => {
46
+ if (!isRecord(candidate)) return candidate;
47
+ const name = toolName(candidate);
48
+ if ((name === "web_search" || name === "image_generation")
49
+ && options.ownsNativeTool?.(name) === false) return candidate;
50
+ if (name === "image_generation" && options.imageGeneration !== false) {
51
+ rewritten.push(name);
52
+ if (options.imageGeneration === "standalone") {
53
+ return createCodexReservedNamespaceTool("image_generation");
54
+ }
55
+ return imageToolConfig(candidate, options);
56
+ }
57
+ if (name === "web_search" && options.webSearch) {
58
+ rewritten.push(name);
59
+ if (typeof options.webSearch === "object" && options.webSearch.implementation === "standalone") {
60
+ return createCodexReservedNamespaceTool("web_search");
61
+ }
62
+ const contentTypes = typeof options.webSearch === "object"
63
+ ? options.webSearch.contentTypes
64
+ : undefined;
65
+ return {
66
+ type: "web_search",
67
+ ...(contentTypes && contentTypes.length > 0
68
+ ? { search_content_types: [...contentTypes] }
69
+ : {}),
70
+ };
71
+ }
72
+ return candidate;
73
+ });
74
+ return { payload: { ...payload, tools } as T, rewritten };
75
+ }
@@ -0,0 +1,23 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ export const CODEX_APPLY_PATCH_DESCRIPTION = "Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.";
4
+
5
+ let grammar: string | undefined;
6
+
7
+ export function codexApplyPatchGrammar(): string {
8
+ grammar ??= readFileSync(new URL("./codex-apply-patch.lark", import.meta.url), "utf8");
9
+ return grammar;
10
+ }
11
+
12
+ export function createCodexApplyPatchCustomTool(): Record<string, unknown> {
13
+ return {
14
+ type: "custom",
15
+ name: "apply_patch",
16
+ description: CODEX_APPLY_PATCH_DESCRIPTION,
17
+ format: {
18
+ type: "grammar",
19
+ syntax: "lark",
20
+ definition: codexApplyPatchGrammar(),
21
+ },
22
+ };
23
+ }
@@ -0,0 +1,19 @@
1
+ start: begin_patch hunk+ end_patch
2
+ begin_patch: "*** Begin Patch" LF
3
+ end_patch: "*** End Patch" LF?
4
+
5
+ hunk: add_hunk | delete_hunk | update_hunk
6
+ add_hunk: "*** Add File: " filename LF add_line+
7
+ delete_hunk: "*** Delete File: " filename LF
8
+ update_hunk: "*** Update File: " filename LF change_move? change?
9
+
10
+ filename: /(.+)/
11
+ add_line: "+" /(.*)/ LF -> line
12
+
13
+ change_move: "*** Move to: " filename LF
14
+ change: (change_context | change_line)+ eof_line?
15
+ change_context: ("@@" | "@@ " /(.+)/) LF
16
+ change_line: ("+" | "-" | " ") /(.*)/ LF
17
+ eof_line: "*** End of File" LF
18
+
19
+ %import common.LF
@@ -0,0 +1,52 @@
1
+ import { type Api, type Model } from "@earendil-works/pi-ai/compat";
2
+
3
+ function shortHash(str: string): string {
4
+ let h1 = 0xdeadbeef;
5
+ let h2 = 0x41c6ce57;
6
+ for (let i = 0; i < str.length; i++) {
7
+ const ch = str.charCodeAt(i);
8
+ h1 = Math.imul(h1 ^ ch, 2654435761);
9
+ h2 = Math.imul(h2 ^ ch, 1597334677);
10
+ }
11
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
12
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
13
+ return (h2 >>> 0).toString(36) + (h1 >>> 0).toString(36);
14
+ }
15
+
16
+ function webSocketHeaderIdentity(headers: Headers): string {
17
+ const requestScoped = new Set([
18
+ "x-codex-turn-metadata",
19
+ "x-codex-turn-state",
20
+ "x-codex-window-id",
21
+ ]);
22
+ return shortHash(
23
+ [...headers.entries()]
24
+ .filter(([name]) => !requestScoped.has(name.toLowerCase()))
25
+ .sort(([left], [right]) => left.localeCompare(right))
26
+ .map(([name, value]) => `${name}:${value}`)
27
+ .join("\n"),
28
+ );
29
+ }
30
+
31
+ export function webSocketCacheKey(
32
+ sessionId: string | undefined,
33
+ model: Model<Api>,
34
+ url: string,
35
+ headers: Headers,
36
+ profileHash?: string,
37
+ ): string | undefined {
38
+ return sessionId
39
+ ? `${sessionId}\n${model.provider}\n${model.api}\n${model.id}\n${url}\n${profileHash ?? "no-profile"}\n${webSocketHeaderIdentity(headers)}`
40
+ : undefined;
41
+ }
42
+
43
+ export function webSocketFallbackKey(
44
+ sessionId: string | undefined,
45
+ model: Model<Api>,
46
+ url: string,
47
+ profileHash?: string,
48
+ ): string | undefined {
49
+ return sessionId
50
+ ? `${sessionId}\n${model.provider}\n${model.api}\n${model.id}\n${url}\n${profileHash ?? "no-profile"}`
51
+ : undefined;
52
+ }
@@ -0,0 +1,50 @@
1
+ import type { Api, AssistantMessage, AssistantMessageEventStream, Model, SimpleStreamOptions } from "@earendil-works/pi-ai/compat";
2
+ import { processResponsesStream } from "@oai404iao/pi-codex-runtime/internal/providers/responses/stream";
3
+ import type { CitationSource, WebSearchCitationSource } from "@oai404iao/pi-codex-runtime/internal/providers/responses/types";
4
+ import { mapCodexEvents } from "./events.js";
5
+ import type { ProviderStreamEffects } from "@oai404iao/pi-codex-runtime/internal/providers/openai-codex/stream-effects";
6
+ import type { ServiceTier, StreamEventShape } from "@oai404iao/pi-codex-runtime/internal/providers/openai-codex/types";
7
+ import { applyServiceTierPricing, resolveCodexServiceTier } from "./usage.js";
8
+
9
+ export async function processCapturedResponsesStream<TApi extends Api>(
10
+ events: AsyncIterable<StreamEventShape>,
11
+ output: AssistantMessage,
12
+ stream: AssistantMessageEventStream,
13
+ model: Model<TApi>,
14
+ options: SimpleStreamOptions | undefined,
15
+ sessionKey: string | undefined,
16
+ effects: ProviderStreamEffects,
17
+ cwd: string,
18
+ requestPrompt: string | undefined,
19
+ webSearchCitationSources: ReadonlyArray<WebSearchCitationSource>,
20
+ historicalCitationSources: ReadonlyArray<CitationSource>,
21
+ ): Promise<{ responseId?: string; responseItems: unknown[] }> {
22
+ const responseItems: unknown[] = [];
23
+ let responseId: string | undefined;
24
+ const observe = effects.createEventObserver?.({
25
+ cwd, requestPrompt, signal: options?.signal, output, stream,
26
+ });
27
+ const captureContinuation = async function* (): AsyncIterable<StreamEventShape> {
28
+ for await (const event of mapCodexEvents(events, sessionKey)) {
29
+ if (event.type === "response.created" && event.response?.id) responseId = event.response.id;
30
+ if (event.type === "response.output_item.done" && event.item) responseItems.push(event.item);
31
+ if (
32
+ (event.type === "response.completed" || event.type === "response.incomplete")
33
+ && event.response?.id
34
+ ) {
35
+ responseId = event.response.id;
36
+ }
37
+ await observe?.(event);
38
+ yield event;
39
+ }
40
+ };
41
+ await processResponsesStream(captureContinuation() as AsyncIterable<never>, output, stream, model, {
42
+ serviceTier: (options as { serviceTier?: ServiceTier } | undefined)?.serviceTier,
43
+ resolveServiceTier: resolveCodexServiceTier,
44
+ applyServiceTierPricing: (usage, serviceTier) =>
45
+ applyServiceTierPricing(usage, serviceTier, model as Model<Api>, cwd),
46
+ webSearchCitationSources,
47
+ historicalCitationSources,
48
+ });
49
+ return { responseId: responseId ?? output.responseId, responseItems };
50
+ }
@@ -0,0 +1,61 @@
1
+ export const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
2
+
3
+ export const MAX_RETRIES = 3;
4
+
5
+ export const BASE_DELAY_MS = 1000;
6
+
7
+ export const SSE_RESPONSE_HEADER_TIMEOUT_MS = 20_000;
8
+
9
+ export const WEBSOCKET_CONNECT_TIMEOUT_MS = 15_000;
10
+
11
+ export const WEBSOCKET_PREWARM_TIMEOUT_MS = 15_000;
12
+
13
+ export const WEBSOCKET_IDLE_TIMEOUT_MS = 300_000;
14
+
15
+ export const WEBSOCKET_SEND_TIMEOUT_MS = 300_000;
16
+
17
+ export const WEBSOCKET_EVENT_QUEUE_CAPACITY = 1600;
18
+
19
+ export const DEFAULT_WEBSOCKET_STREAM_MAX_RETRIES = 5;
20
+
21
+ export const MAX_WEBSOCKET_STREAM_MAX_RETRIES = 100;
22
+
23
+ export const WEBSOCKET_RETRY_BASE_DELAY_MS = 200;
24
+
25
+ export const WEBSOCKET_RETRY_MAX_DELAY_MS = 60_000;
26
+
27
+ export const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
28
+
29
+ export const WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE = "websocket_connection_limit_reached";
30
+
31
+ export const PREVIOUS_RESPONSE_NOT_FOUND_CODE = "previous_response_not_found";
32
+
33
+ export const CODEX_RESPONSE_STATUSES = new Set(["completed", "incomplete", "failed", "cancelled", "queued", "in_progress"]);
34
+
35
+ export const OPENAI_BETA_RESPONSES_WEBSOCKETS = "responses_websockets=2026-02-06";
36
+
37
+ export const X_OPENAI_INTERNAL_CODEX_RESPONSES_LITE = "x-openai-internal-codex-responses-lite";
38
+
39
+ export const WS_RESPONSES_LITE_CLIENT_METADATA_KEY = "ws_request_header_x_openai_internal_codex_responses_lite";
40
+
41
+ export const WS_STREAM_REQUEST_START_MS_CLIENT_METADATA_KEY = "x-codex-ws-stream-request-start-ms";
42
+
43
+ export const WEB_SEARCH_SOURCES_INCLUDE = "web_search_call.action.sources";
44
+
45
+ export const WEB_SEARCH_RESULTS_INCLUDE = "web_search_call.results";
46
+
47
+ export const SESSION_WEBSOCKET_CACHE_TTL_MS = 5 * 60 * 1000;
48
+
49
+ export const CODEX_COMPACTION_TRIGGER_TYPE = "compaction_trigger";
50
+
51
+ export const CODEX_RETAINED_MESSAGE_TOKEN_BUDGET = 64_000;
52
+
53
+ export const CODEX_MAX_RETAINED_AGENT_MESSAGE_TOKENS = 10_000;
54
+
55
+ export const CODEX_REMOTE_COMPACTION_STREAM_RETRIES = 2;
56
+
57
+ export const X_CODEX_BETA_FEATURES = "x-codex-beta-features";
58
+
59
+ export const CODEX_REMOTE_COMPACTION_V2_FEATURE = "remote_compaction_v2";
60
+
61
+ export const APPROX_BYTES_PER_TOKEN = 4;
@@ -0,0 +1,110 @@
1
+ import { type CachedWebSocketContinuationState, type ResponsesBody, type SessionWebSocketCacheEntry } from "@oai404iao/pi-codex-runtime/internal/providers/openai-codex/types";
2
+
3
+ function requestBodyWithoutInput(body: ResponsesBody): ResponsesBody {
4
+ const {
5
+ input: _input,
6
+ previous_response_id: _previousResponseId,
7
+ client_metadata: _clientMetadata,
8
+ stream_options: _streamOptions,
9
+ generate: _generate,
10
+ ...rest
11
+ } = body;
12
+ return rest as ResponsesBody;
13
+ }
14
+
15
+ function normalizeResponseItemForComparison(value: unknown): unknown {
16
+ if (Array.isArray(value)) return value.map(normalizeResponseItemForComparison);
17
+ if (!value || typeof value !== "object") return value;
18
+ const result: Record<string, unknown> = {};
19
+ for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
20
+ if (key === "internal_chat_message_metadata_passthrough") continue;
21
+ result[key] = normalizeResponseItemForComparison(entry);
22
+ }
23
+ return result;
24
+ }
25
+
26
+ function stableJson(value: unknown): string {
27
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
28
+ if (value && typeof value === "object") {
29
+ return `{${Object.entries(value as Record<string, unknown>)
30
+ .sort(([left], [right]) => left.localeCompare(right))
31
+ .map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
32
+ .join(",")}}`;
33
+ }
34
+ return JSON.stringify(value) ?? "undefined";
35
+ }
36
+
37
+ function responseInputsEqual(a: unknown[] | undefined, b: unknown[] | undefined): boolean {
38
+ const left = a ?? [];
39
+ const right = b ?? [];
40
+ if (left.length !== right.length) return false;
41
+ return left.every((item, index) =>
42
+ stableJson(normalizeResponseItemForComparison(item))
43
+ === stableJson(normalizeResponseItemForComparison(right[index])));
44
+ }
45
+
46
+ function requestBodiesMatchExceptInput(a: ResponsesBody, b: ResponsesBody): boolean {
47
+ return stableJson(requestBodyWithoutInput(a)) === stableJson(requestBodyWithoutInput(b));
48
+ }
49
+
50
+ function getCachedWebSocketInputDelta(body: ResponsesBody, continuation: CachedWebSocketContinuationState): unknown[] | undefined {
51
+ if (!requestBodiesMatchExceptInput(body, continuation.lastRequestBody)) {
52
+ return undefined;
53
+ }
54
+
55
+ const currentInput = body.input ?? [];
56
+ const baseline = [...(continuation.lastRequestBody.input ?? []), ...continuation.lastResponseItems];
57
+ if (currentInput.length < baseline.length) {
58
+ return undefined;
59
+ }
60
+
61
+ const prefix = currentInput.slice(0, baseline.length);
62
+ if (!responseInputsEqual(prefix, baseline)) {
63
+ return undefined;
64
+ }
65
+
66
+ return currentInput.slice(baseline.length);
67
+ }
68
+
69
+ export function buildCachedWebSocketRequestBody(
70
+ entry: SessionWebSocketCacheEntry,
71
+ body: ResponsesBody,
72
+ ): ResponsesBody {
73
+ const continuation = entry.continuation;
74
+ if (!continuation) {
75
+ return body;
76
+ }
77
+
78
+ const delta = getCachedWebSocketInputDelta(body, continuation);
79
+ if (delta === undefined || !continuation.lastResponseId) {
80
+ entry.continuation = undefined;
81
+ return body;
82
+ }
83
+ return {
84
+ ...body,
85
+ previous_response_id: continuation.lastResponseId,
86
+ input: delta,
87
+ };
88
+ }
89
+
90
+ function isPrefixedResponseItemId(value: string): boolean {
91
+ const separator = value.indexOf("_");
92
+ return separator > 0 && separator < value.length - 1;
93
+ }
94
+
95
+ function prepareResponseItemsForWire(items: unknown[]): unknown[] {
96
+ return items.map((item) => {
97
+ if (!item || typeof item !== "object" || Array.isArray(item)) return item;
98
+ const record = item as Record<string, unknown>;
99
+ if (typeof record.id !== "string" || isPrefixedResponseItemId(record.id)) return item;
100
+ const { id: _id, ...rest } = record;
101
+ return rest;
102
+ });
103
+ }
104
+
105
+ export function prepareWebSocketRequestBodyForWire(body: ResponsesBody): ResponsesBody {
106
+ return {
107
+ ...body,
108
+ input: prepareResponseItemsForWire(body.input ?? []),
109
+ };
110
+ }