@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,130 @@
1
+ import { type StreamEventShape } from "@oai404iao/pi-codex-runtime/internal/providers/openai-codex/types";
2
+
3
+ export class NonRetryableProviderError extends Error {}
4
+
5
+ export class ProviderResponseError extends Error {
6
+ code?: string;
7
+ errorType?: string;
8
+ status?: number;
9
+ retryAfterMs?: number;
10
+ }
11
+
12
+ export class ProviderProtocolError extends Error {}
13
+
14
+ export class WebSocketHandshakeError extends Error {
15
+ constructor(
16
+ public readonly status: number,
17
+ message: string,
18
+ public readonly headers: Record<string, string> = {},
19
+ public readonly body?: string,
20
+ ) {
21
+ super(withHttpStatusPrefix(status, message));
22
+ this.name = "WebSocketHandshakeError";
23
+ }
24
+ }
25
+
26
+ const HTTP_STATUS_MESSAGE_PREFIX = /^HTTP\s+\d{3}(?::|\b)/i;
27
+
28
+ export function isRetryableError(status: number, errorText: string): boolean {
29
+ if (status === 429 || status === 500 || status === 502 || status === 503 || status === 504) {
30
+ return true;
31
+ }
32
+ return /rate.?limit|overloaded|service.?unavailable|upstream.?connect|connection.?refused/i.test(errorText);
33
+ }
34
+
35
+ export function withHttpStatusPrefix(status: number, message: string): string {
36
+ const trimmed = message.trim() || "Request failed";
37
+ if (HTTP_STATUS_MESSAGE_PREFIX.test(trimmed)) return trimmed;
38
+ return `HTTP ${status}: ${trimmed}`;
39
+ }
40
+
41
+ export function extractWebSocketError(event: unknown): Error {
42
+ if (event && typeof event === "object") {
43
+ const message = "message" in event ? (event as { message?: unknown }).message : undefined;
44
+ if (typeof message === "string" && message.length > 0) {
45
+ return new Error(message);
46
+ }
47
+ const nestedError = "error" in event ? (event as { error?: unknown }).error : undefined;
48
+ if (nestedError instanceof Error && nestedError.message.length > 0) {
49
+ return nestedError;
50
+ }
51
+ if (nestedError && typeof nestedError === "object" && "message" in nestedError) {
52
+ const nestedMessage = (nestedError as { message?: unknown }).message;
53
+ if (typeof nestedMessage === "string" && nestedMessage.length > 0) {
54
+ return new Error(nestedMessage);
55
+ }
56
+ }
57
+ }
58
+ return new Error("WebSocket error");
59
+ }
60
+
61
+ export function extractWebSocketCloseError(event: unknown): Error {
62
+ if (event && typeof event === "object") {
63
+ const code = "code" in event ? (event as { code?: unknown }).code : undefined;
64
+ const reason = "reason" in event ? (event as { reason?: unknown }).reason : undefined;
65
+ const codeText = typeof code === "number" ? ` ${code}` : "";
66
+ const reasonText = typeof reason === "string" && reason.length > 0 ? ` ${reason}` : "";
67
+ return new Error(`WebSocket closed${codeText}${reasonText}`.trim());
68
+ }
69
+ return new Error("WebSocket closed");
70
+ }
71
+
72
+ export function friendlyUsageLimitMessage(error: StreamEventShape["error"], status: number | undefined): string | undefined {
73
+ const code = error?.code ?? error?.type ?? "";
74
+ if (!/usage_limit_reached|usage_not_included/i.test(code)) {
75
+ return undefined;
76
+ }
77
+ const plan = error?.plan_type ? ` (${error.plan_type.toLowerCase()} plan)` : "";
78
+ const mins = error?.resets_at
79
+ ? Math.max(0, Math.round((error.resets_at * 1000 - Date.now()) / 60_000))
80
+ : undefined;
81
+ const when = mins !== undefined ? ` Try again in ~${mins} min.` : "";
82
+ return `You have hit your OpenAI usage limit${plan}.${when}`.trim();
83
+ }
84
+
85
+ export function buildProviderErrorMessage(error: unknown): string {
86
+ const message = error instanceof Error ? error.message : String(error);
87
+ const candidate = error as { code?: unknown; errorType?: unknown; status?: unknown };
88
+ if (
89
+ candidate?.code === "stream_read_error"
90
+ && candidate.errorType === "upstream_error"
91
+ ) {
92
+ // Pi's agent-level retry classifier recognizes "connection error". Surface
93
+ // this upstream SSE read failure in that category instead of
94
+ // retrying inside the provider, so Pi's retry settings and UI remain the
95
+ // single source of truth.
96
+ return `Connection error: ${message}`;
97
+ }
98
+ if (/^(?:WebSocket (?:error|closed)|WebSocket stream closed before response\.completed|Stream closed before response\.completed)/.test(message)) {
99
+ return `Connection error: ${message}`;
100
+ }
101
+ if (typeof candidate?.status === "number") {
102
+ return withHttpStatusPrefix(candidate.status, message);
103
+ }
104
+ return message;
105
+ }
106
+
107
+ export async function parseErrorResponse(response: Response): Promise<{ message: string; friendlyMessage?: string }> {
108
+ const raw = await response.text();
109
+ let message = raw || response.statusText || "Request failed";
110
+ let friendlyMessage: string | undefined;
111
+
112
+ try {
113
+ const parsed = JSON.parse(raw) as { error?: { code?: string; type?: string; plan_type?: string; resets_at?: number; message?: string } };
114
+ const err = parsed?.error;
115
+ if (err) {
116
+ const code = err.code || err.type || "";
117
+ if (/usage_limit_reached|usage_not_included|rate_limit_exceeded/i.test(code) || response.status === 429) {
118
+ const plan = err.plan_type ? ` (${err.plan_type.toLowerCase()} plan)` : "";
119
+ const mins = err.resets_at ? Math.max(0, Math.round((err.resets_at * 1000 - Date.now()) / 60000)) : undefined;
120
+ const when = mins !== undefined ? ` Try again in ~${mins} min.` : "";
121
+ friendlyMessage = `You have hit your ChatGPT usage limit${plan}.${when}`.trim();
122
+ }
123
+ message = err.message || friendlyMessage || message;
124
+ }
125
+ } catch {
126
+ // ignore malformed error bodies
127
+ }
128
+
129
+ return { message, friendlyMessage };
130
+ }
@@ -0,0 +1,123 @@
1
+ import { captureCodexTurnState } from "@oai404iao/pi-codex-runtime/internal/codex-wire-identity";
2
+ import { CODEX_RESPONSE_STATUSES } from "./constants.js";
3
+ import { ProviderResponseError, friendlyUsageLimitMessage } from "./errors.js";
4
+ import { retryAfterMsFromHeaders } from "./retry.js";
5
+ import { type StreamEventShape } from "@oai404iao/pi-codex-runtime/internal/providers/openai-codex/types";
6
+
7
+ export async function* mapCodexEvents(
8
+ events: AsyncIterable<StreamEventShape>,
9
+ sessionKey?: string,
10
+ ): AsyncIterable<StreamEventShape> {
11
+ let sawTerminalResponse = false;
12
+ const completedOutputItems = new Set<string>();
13
+ const outputItemKey = (item: unknown, index?: number): string | undefined => {
14
+ if (!item || typeof item !== "object") return undefined;
15
+ const candidate = item as { id?: unknown; type?: unknown };
16
+ if (typeof candidate.id === "string" && typeof candidate.type === "string") return `${candidate.type}:${candidate.id}`;
17
+ return typeof candidate.type === "string" && typeof index === "number" ? `${candidate.type}:index:${index}` : undefined;
18
+ };
19
+ for await (const event of events) {
20
+ const type = typeof event.type === "string" ? event.type : undefined;
21
+ if (!type) continue;
22
+
23
+ if (type === "response.metadata" && sessionKey) {
24
+ const eventHeaders = event.headers && typeof event.headers === "object" && !Array.isArray(event.headers)
25
+ ? (event.headers as Record<string, unknown>)
26
+ : undefined;
27
+ const turnState = eventHeaders?.["x-codex-turn-state"];
28
+ if (typeof turnState === "string") {
29
+ captureCodexTurnState(sessionKey, turnState);
30
+ }
31
+ }
32
+
33
+ if (type === "error") {
34
+ const nestedError = event.error;
35
+ const status = typeof event.status === "number"
36
+ ? event.status
37
+ : typeof event.status_code === "number"
38
+ ? event.status_code
39
+ : undefined;
40
+ const eventHeaders = event.headers && typeof event.headers === "object" && !Array.isArray(event.headers)
41
+ ? Object.fromEntries(
42
+ Object.entries(event.headers as Record<string, unknown>)
43
+ .filter((entry): entry is [string, string | number | boolean] =>
44
+ typeof entry[1] === "string" || typeof entry[1] === "number" || typeof entry[1] === "boolean")
45
+ .map(([name, value]) => [name, String(value)]),
46
+ )
47
+ : undefined;
48
+ const code = typeof nestedError?.code === "string"
49
+ ? nestedError.code
50
+ : typeof event.code === "string"
51
+ ? event.code
52
+ : undefined;
53
+ const message = typeof nestedError?.message === "string"
54
+ ? nestedError.message
55
+ : typeof event.message === "string"
56
+ ? event.message
57
+ : undefined;
58
+ const displayMessage = friendlyUsageLimitMessage(nestedError, status)
59
+ ?? `Codex error: ${message || code || JSON.stringify(event)}`;
60
+ const error = new ProviderResponseError(displayMessage) as ProviderResponseError & {
61
+ sequenceNumber?: number;
62
+ };
63
+ if (code) error.code = code;
64
+ if (typeof nestedError?.type === "string") error.errorType = nestedError.type;
65
+ if (status !== undefined) error.status = status;
66
+ error.retryAfterMs = retryAfterMsFromHeaders(eventHeaders);
67
+ if (typeof event.sequence_number === "number") error.sequenceNumber = event.sequence_number;
68
+ throw error;
69
+ }
70
+
71
+ if (type === "response.failed") {
72
+ const responseError = event.response?.error as { code?: unknown; message?: unknown } | undefined;
73
+ const error = new ProviderResponseError(
74
+ typeof responseError?.message === "string" ? responseError.message : "OpenAI Responses request failed",
75
+ );
76
+ if (typeof responseError?.code === "string") error.code = responseError.code;
77
+ throw error;
78
+ }
79
+
80
+ if (type === "response.done" || type === "response.completed" || type === "response.incomplete") {
81
+ sawTerminalResponse = true;
82
+ const response = event.response;
83
+ const output = Array.isArray(response?.output) ? response.output : [];
84
+ for (let outputIndex = 0; outputIndex < output.length; outputIndex++) {
85
+ const item = output[outputIndex];
86
+ if (!item || typeof item !== "object") continue;
87
+ const itemType = (item as { type?: unknown }).type;
88
+ if (
89
+ itemType !== "image_generation_call"
90
+ && itemType !== "web_search_call"
91
+ && itemType !== "compaction"
92
+ && itemType !== "context_compaction"
93
+ ) continue;
94
+ const key = outputItemKey(item, outputIndex);
95
+ if (key && completedOutputItems.has(key)) continue;
96
+ if (key) completedOutputItems.add(key);
97
+ yield { type: "response.output_item.done", output_index: outputIndex, item } as StreamEventShape;
98
+ }
99
+ yield {
100
+ ...event,
101
+ type: "response.completed",
102
+ response: response ? { ...response, status: normalizeCodexStatus(response.status) } : response,
103
+ };
104
+ return;
105
+ }
106
+
107
+ if (type === "response.output_item.done") {
108
+ const key = outputItemKey(event.item, typeof event.output_index === "number" ? event.output_index : undefined);
109
+ if (key) completedOutputItems.add(key);
110
+ }
111
+
112
+ yield event;
113
+ }
114
+
115
+ if (!sawTerminalResponse) {
116
+ throw new Error("Stream closed before response.completed");
117
+ }
118
+ }
119
+
120
+ function normalizeCodexStatus(status: string | undefined): string | undefined {
121
+ if (typeof status !== "string") return undefined;
122
+ return CODEX_RESPONSE_STATUSES.has(status) ? status : undefined;
123
+ }
@@ -0,0 +1,224 @@
1
+ import { type ProviderHeaders } from "@earendil-works/pi-ai";
2
+ import { type Api, type Model } from "@earendil-works/pi-ai/compat";
3
+ import { type CodexRequestProfile } from "@oai404iao/pi-codex-runtime/internal/codex-request-profile";
4
+ import { codexTurnStateFor, resolveCodexWireIdentity, type CodexRequestIdentity } from "@oai404iao/pi-codex-runtime/internal/codex-wire-identity";
5
+ import { type ResolvedCodexModelSettings } from "@oai404iao/pi-codex-runtime/internal/model-catalog/runtime";
6
+ import { isProviderHeaderSuppressed, mergeProviderHeaders, providerHeaderDirective, setProviderDefaultHeader, setProviderGeneratedHeader } from "@oai404iao/pi-codex-runtime/internal/provider-headers";
7
+ import { CODEX_REMOTE_COMPACTION_V2_FEATURE, OPENAI_BETA_RESPONSES_WEBSOCKETS, X_CODEX_BETA_FEATURES, X_OPENAI_INTERNAL_CODEX_RESPONSES_LITE } from "./constants.js";
8
+ import { buildCodexTurnMetadataJson } from "./request-metadata.js";
9
+ import { dynamicImport } from "./runtime.js";
10
+
11
+ let _os: { platform(): string; release(): string; arch(): string } | null = null;
12
+
13
+ if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
14
+ dynamicImport("node:os")
15
+ .then((module) => {
16
+ _os = module;
17
+ })
18
+ .catch(() => {
19
+ _os = null;
20
+ });
21
+ }
22
+
23
+ export function headersToRecord(headers: Headers): Record<string, string> {
24
+ return Object.fromEntries(headers.entries());
25
+ }
26
+
27
+ export function providerHeadersToHeaders(headers: ProviderHeaders): Headers {
28
+ const result = new Headers();
29
+ for (const [name, value] of Object.entries(headers)) {
30
+ if (typeof value === "string") result.set(name, value);
31
+ }
32
+ return result;
33
+ }
34
+
35
+ function buildBaseCodexHeaders(
36
+ modelHeaders: ProviderHeaders | undefined,
37
+ additionalHeaders: ProviderHeaders | undefined,
38
+ accountId: string | undefined,
39
+ token: string,
40
+ ): Headers {
41
+ const headers = mergeProviderHeaders(modelHeaders, additionalHeaders);
42
+ if (providerHeaderDirective(additionalHeaders, "authorization") === undefined && token) {
43
+ setProviderGeneratedHeader(headers, "Authorization", `Bearer ${token}`);
44
+ }
45
+ if (accountId) setProviderDefaultHeader(headers, "chatgpt-account-id", accountId);
46
+ setProviderDefaultHeader(headers, "originator", "pi");
47
+ setProviderDefaultHeader(
48
+ headers,
49
+ "User-Agent",
50
+ _os ? `pi (${_os.platform()} ${_os.release()}; ${_os.arch()})` : "pi (browser)",
51
+ );
52
+ return headers;
53
+ }
54
+
55
+ /**
56
+ * Resolve the wire identity (UUID v7 session/thread/window) for a pi session
57
+ * and thread. Returns `undefined` when no pi session id is available so
58
+ * session-less requests keep their previous behavior.
59
+ */
60
+ function wireIdentityFor(
61
+ sessionId: string | undefined,
62
+ threadId?: string,
63
+ ): { sessionId: string; threadId: string; windowId: string } | undefined {
64
+ if (!sessionId) return undefined;
65
+ return resolveCodexWireIdentity(sessionId, threadId || sessionId);
66
+ }
67
+
68
+ /** Inject the Codex-compatible identity headers for a session. */
69
+ function applyWireIdentityHeaders(
70
+ headers: Headers,
71
+ sessionId: string | undefined,
72
+ threadId: string | undefined,
73
+ requestIdentity?: CodexRequestIdentity,
74
+ ): void {
75
+ const wire = requestIdentity ?? wireIdentityFor(sessionId, threadId);
76
+ if (!wire) return;
77
+ setProviderGeneratedHeader(headers, "session-id", wire.sessionId);
78
+ setProviderGeneratedHeader(headers, "thread-id", wire.threadId);
79
+ setProviderGeneratedHeader(headers, "x-codex-window-id", wire.windowId);
80
+ setProviderGeneratedHeader(headers, "x-client-request-id", wire.threadId);
81
+ const turnState = requestIdentity?.turnState
82
+ ?? (sessionId ? codexTurnStateFor(sessionId) : undefined);
83
+ if (turnState) {
84
+ setProviderGeneratedHeader(headers, "x-codex-turn-state", turnState);
85
+ }
86
+ if (requestIdentity?.parentThreadId) {
87
+ setProviderGeneratedHeader(
88
+ headers,
89
+ "x-codex-parent-thread-id",
90
+ requestIdentity.parentThreadId,
91
+ );
92
+ }
93
+ if (requestIdentity?.subagentKind) {
94
+ setProviderGeneratedHeader(
95
+ headers,
96
+ "x-openai-subagent",
97
+ requestIdentity.subagentKind,
98
+ );
99
+ }
100
+ if (requestIdentity) {
101
+ setProviderGeneratedHeader(
102
+ headers,
103
+ "x-codex-turn-metadata",
104
+ buildCodexTurnMetadataJson(requestIdentity),
105
+ );
106
+ }
107
+ }
108
+
109
+ /** Codex identity headers for an SSE request (exported for tests). */
110
+ export function buildSSEHeaders(
111
+ modelHeaders: ProviderHeaders | undefined,
112
+ additionalHeaders: ProviderHeaders | undefined,
113
+ accountId: string | undefined,
114
+ token: string,
115
+ sessionId: string | undefined,
116
+ profile: CodexRequestProfile,
117
+ threadId?: string,
118
+ requestIdentity?: CodexRequestIdentity,
119
+ ): Headers {
120
+ const headers = buildBaseCodexHeaders(modelHeaders, additionalHeaders, accountId, token);
121
+ setProviderDefaultHeader(headers, "OpenAI-Beta", "responses=experimental");
122
+ setProviderDefaultHeader(headers, "accept", "text/event-stream");
123
+ setProviderDefaultHeader(headers, "content-type", "application/json");
124
+ if (profile.responsesMode === "lite") {
125
+ setProviderDefaultHeader(headers, X_OPENAI_INTERNAL_CODEX_RESPONSES_LITE, "true");
126
+ }
127
+
128
+ applyWireIdentityHeaders(headers, sessionId, threadId, requestIdentity);
129
+
130
+ return headers;
131
+ }
132
+
133
+ function appendCommaSeparatedHeader(headers: Headers, name: string, value: string): void {
134
+ if (isProviderHeaderSuppressed(headers, name)) return;
135
+ const values = (headers.get(name) ?? "")
136
+ .split(",")
137
+ .map((entry) => entry.trim())
138
+ .filter(Boolean);
139
+ if (!values.includes(value)) headers.set(name, [...values, value].join(","));
140
+ }
141
+
142
+ export function applyConfiguredResponsesFeatureHeaders(
143
+ headers: Headers,
144
+ settings: ResolvedCodexModelSettings,
145
+ _model: Model<Api>,
146
+ ): Headers {
147
+ if (settings.compactionMode === "responses") {
148
+ appendCommaSeparatedHeader(
149
+ headers,
150
+ X_CODEX_BETA_FEATURES,
151
+ CODEX_REMOTE_COMPACTION_V2_FEATURE,
152
+ );
153
+ }
154
+ return headers;
155
+ }
156
+
157
+ export function buildWebSocketHeaders(
158
+ modelHeaders: ProviderHeaders | undefined,
159
+ additionalHeaders: ProviderHeaders | undefined,
160
+ accountId: string | undefined,
161
+ token: string,
162
+ sessionId: string,
163
+ threadId = sessionId,
164
+ requestIdentity?: CodexRequestIdentity,
165
+ ): Headers {
166
+ const headers = buildBaseCodexHeaders(modelHeaders, additionalHeaders, accountId, token);
167
+ headers.delete("accept");
168
+ headers.delete("content-type");
169
+ headers.delete("OpenAI-Beta");
170
+ headers.delete("openai-beta");
171
+ setProviderDefaultHeader(headers, "OpenAI-Beta", OPENAI_BETA_RESPONSES_WEBSOCKETS);
172
+ setProviderDefaultHeader(headers, "x-client-request-id", threadId);
173
+ setProviderDefaultHeader(headers, "session-id", sessionId);
174
+ setProviderDefaultHeader(headers, "thread-id", threadId);
175
+ const wire = requestIdentity ?? wireIdentityFor(sessionId, threadId);
176
+ if (wire) {
177
+ setProviderGeneratedHeader(headers, "session-id", wire.sessionId);
178
+ setProviderGeneratedHeader(headers, "thread-id", wire.threadId);
179
+ setProviderGeneratedHeader(headers, "x-codex-window-id", wire.windowId);
180
+ setProviderGeneratedHeader(headers, "x-client-request-id", wire.threadId);
181
+ }
182
+ if (requestIdentity?.parentThreadId) {
183
+ setProviderGeneratedHeader(
184
+ headers,
185
+ "x-codex-parent-thread-id",
186
+ requestIdentity.parentThreadId,
187
+ );
188
+ }
189
+ if (requestIdentity?.subagentKind) {
190
+ setProviderGeneratedHeader(
191
+ headers,
192
+ "x-openai-subagent",
193
+ requestIdentity.subagentKind,
194
+ );
195
+ }
196
+ if (requestIdentity) {
197
+ setProviderGeneratedHeader(
198
+ headers,
199
+ "x-codex-turn-metadata",
200
+ buildCodexTurnMetadataJson(requestIdentity),
201
+ );
202
+ }
203
+ return headers;
204
+ }
205
+
206
+ export function buildJsonHeaders(
207
+ modelHeaders: ProviderHeaders | undefined,
208
+ additionalHeaders: ProviderHeaders | undefined,
209
+ accountId: string | undefined,
210
+ apiKey: string,
211
+ sessionId?: string,
212
+ requestIdentity?: CodexRequestIdentity,
213
+ ): Headers {
214
+ const headers = buildBaseCodexHeaders(modelHeaders, additionalHeaders, accountId, apiKey);
215
+ setProviderDefaultHeader(headers, "accept", "application/json");
216
+ setProviderDefaultHeader(headers, "content-type", "application/json");
217
+ applyWireIdentityHeaders(
218
+ headers,
219
+ sessionId,
220
+ requestIdentity?.threadId ?? sessionId,
221
+ requestIdentity,
222
+ );
223
+ return headers;
224
+ }
@@ -0,0 +1,24 @@
1
+ import { type CodexRequestProfile } from "@oai404iao/pi-codex-runtime/internal/codex-request-profile";
2
+ import { WS_RESPONSES_LITE_CLIENT_METADATA_KEY } from "./constants.js";
3
+
4
+ export function stripResponsesLiteImageDetails(value: unknown): void {
5
+ if (Array.isArray(value)) {
6
+ for (const item of value) stripResponsesLiteImageDetails(item);
7
+ return;
8
+ }
9
+ if (!value || typeof value !== "object") return;
10
+ const record = value as Record<string, unknown>;
11
+ if (record.type === "input_image") delete record.detail;
12
+ for (const entry of Object.values(record)) stripResponsesLiteImageDetails(entry);
13
+ }
14
+
15
+ export function withResponsesLiteWebSocketMetadata<T extends { client_metadata?: Record<string, string> }>(body: T, responsesMode: CodexRequestProfile["responsesMode"]): T {
16
+ if (responsesMode !== "lite") return body;
17
+ return {
18
+ ...body,
19
+ client_metadata: {
20
+ ...body.client_metadata,
21
+ [WS_RESPONSES_LITE_CLIENT_METADATA_KEY]: "true",
22
+ },
23
+ };
24
+ }
@@ -0,0 +1,33 @@
1
+ import { type Api, type AssistantMessage, type Model } from "@earendil-works/pi-ai/compat";
2
+ import { buildProviderErrorMessage } from "./errors.js";
3
+
4
+ export function createInitialAssistantMessage<TApi extends Api>(model: Model<TApi>): AssistantMessage {
5
+ return {
6
+ role: "assistant",
7
+ content: [],
8
+ api: model.api,
9
+ provider: model.provider,
10
+ model: model.id,
11
+ usage: {
12
+ input: 0,
13
+ output: 0,
14
+ cacheRead: 0,
15
+ cacheWrite: 0,
16
+ totalTokens: 0,
17
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
18
+ },
19
+ stopReason: "stop",
20
+ timestamp: Date.now(),
21
+ };
22
+ }
23
+
24
+ export function createErrorMessage(message: AssistantMessage, error: unknown, aborted: boolean): AssistantMessage {
25
+ for (const block of message.content) {
26
+ if (typeof block === "object" && block !== null && "partialJson" in block) {
27
+ delete (block as { partialJson?: string }).partialJson;
28
+ }
29
+ }
30
+ message.stopReason = aborted ? "aborted" : "error";
31
+ message.errorMessage = buildProviderErrorMessage(error);
32
+ return message;
33
+ }
@@ -0,0 +1,76 @@
1
+ import { WEBSOCKET_CONNECT_TIMEOUT_MS, WEBSOCKET_SEND_TIMEOUT_MS } from "./constants.js";
2
+ import { prepareWebSocketRequestBodyForWire } from "./continuation.js";
3
+ import { mapCodexEvents } from "./events.js";
4
+ import { withWebSocketRequestMetadata } from "./request-metadata.js";
5
+ import { type ResponsesBody, type WebSocketPrewarmRequest } from "@oai404iao/pi-codex-runtime/internal/providers/openai-codex/types";
6
+ import { parseWebSocket, sendWebSocketRequest } from "./websocket-events.js";
7
+ import { acquireWebSocket } from "./websocket-session.js";
8
+
9
+ export async function prewarmWebSocket(request: WebSocketPrewarmRequest): Promise<void> {
10
+ if ((request.body.input?.length ?? 0) > 0) {
11
+ throw new Error("Startup WebSocket prewarm must not include conversation input");
12
+ }
13
+ const acquired = await acquireWebSocket(
14
+ request.url,
15
+ request.headers,
16
+ request.cacheKey,
17
+ request.requestMetadata.sessionId,
18
+ request.signal,
19
+ request.connectTimeoutMs ?? WEBSOCKET_CONNECT_TIMEOUT_MS,
20
+ );
21
+ const { socket, entry } = acquired;
22
+ let keepConnection = true;
23
+ let released = false;
24
+ const releaseOnce = (options?: { keep?: boolean }) => {
25
+ if (released) return;
26
+ released = true;
27
+ acquired.release(options);
28
+ };
29
+
30
+ try {
31
+ const fullBody = withWebSocketRequestMetadata(request.body, request.requestMetadata);
32
+ const prewarmBody: ResponsesBody = {
33
+ ...fullBody,
34
+ generate: false,
35
+ };
36
+ // A startup prewarm is the root of a new session continuation. Never
37
+ // chain another generate:false request from an existing response.
38
+ if (entry?.continuation) entry.continuation = undefined;
39
+ const requestBody = prewarmBody;
40
+ const wireBody = prepareWebSocketRequestBodyForWire(requestBody);
41
+ if (request.signal?.aborted) throw new Error("Request was aborted");
42
+ await sendWebSocketRequest(
43
+ socket,
44
+ JSON.stringify({ type: "response.create", ...wireBody }),
45
+ request.signal,
46
+ WEBSOCKET_SEND_TIMEOUT_MS,
47
+ );
48
+ const responseItems: unknown[] = [];
49
+ let responseId: string | undefined;
50
+ for await (const event of mapCodexEvents(parseWebSocket(socket, request.signal), request.requestMetadata.sessionId)) {
51
+ if (event.type === "response.created" && event.response?.id) responseId = event.response.id;
52
+ if (event.type === "response.output_item.done" && event.item) responseItems.push(event.item);
53
+ if (
54
+ (event.type === "response.completed" || event.type === "response.incomplete")
55
+ && event.response?.id
56
+ ) {
57
+ responseId = event.response.id;
58
+ }
59
+ }
60
+ if (entry && responseId) {
61
+ entry.continuation = {
62
+ lastRequestBody: prewarmBody,
63
+ lastResponseId: responseId,
64
+ lastResponseItems: responseItems,
65
+ };
66
+ }
67
+ releaseOnce({ keep: true });
68
+ } catch (error) {
69
+ keepConnection = false;
70
+ if (entry) entry.continuation = undefined;
71
+ releaseOnce({ keep: false });
72
+ throw error;
73
+ } finally {
74
+ releaseOnce({ keep: keepConnection });
75
+ }
76
+ }
@@ -0,0 +1,55 @@
1
+ import { ProxyAgent, type Dispatcher } from "undici";
2
+
3
+ function envFirst(names: string[]): string | undefined {
4
+ if (typeof process === "undefined") return undefined;
5
+ for (const name of names) {
6
+ const value = process.env[name];
7
+ if (value?.trim()) return value.trim();
8
+ }
9
+ return undefined;
10
+ }
11
+
12
+ function noProxyMatches(hostname: string, noProxy: string | undefined): boolean {
13
+ if (!noProxy) return false;
14
+ const host = hostname.toLowerCase();
15
+ for (const rawPart of noProxy.split(",")) {
16
+ const part = rawPart.trim().toLowerCase();
17
+ if (!part) continue;
18
+ if (part === "*") return true;
19
+ const normalized = part.startsWith(".") ? part.slice(1) : part;
20
+ if (host === normalized || host.endsWith(`.${normalized}`)) return true;
21
+ }
22
+ return false;
23
+ }
24
+
25
+ export function proxyForWebSocketUrl(rawUrl: string): string | undefined {
26
+ let url: URL;
27
+ try {
28
+ url = new URL(rawUrl);
29
+ } catch {
30
+ return undefined;
31
+ }
32
+ const noProxy = envFirst(["NO_PROXY", "no_proxy"]);
33
+ if (noProxyMatches(url.hostname, noProxy)) return undefined;
34
+ if (url.protocol === "wss:" || url.protocol === "https:") {
35
+ return envFirst(["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy"]);
36
+ }
37
+ if (url.protocol === "ws:" || url.protocol === "http:") {
38
+ return envFirst(["HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy", "ALL_PROXY", "all_proxy"]);
39
+ }
40
+ return undefined;
41
+ }
42
+
43
+ export async function proxyDispatcherForUrl(rawUrl: string): Promise<Dispatcher | undefined> {
44
+ const proxy = proxyForWebSocketUrl(rawUrl);
45
+ if (!proxy) return undefined;
46
+ return new ProxyAgent(proxy);
47
+ }
48
+
49
+ export async function webSocketOptionsForUrl(url: string, headers: Record<string, string>): Promise<{
50
+ headers: Record<string, string>;
51
+ dispatcher?: Dispatcher;
52
+ }> {
53
+ const dispatcher = await proxyDispatcherForUrl(url);
54
+ return dispatcher ? { headers, dispatcher } : { headers };
55
+ }