@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,51 @@
1
+ import { NonRetryableProviderError } from "../../providers/openai-codex/errors.js";
2
+ import { mapCodexEvents } from "../../providers/openai-codex/events.js";
3
+ import { parseSSE } from "../../providers/openai-codex/sse.js";
4
+ import { type StreamEventShape } from "@oai404iao/pi-codex-runtime/internal/providers/openai-codex/types";
5
+ import { isNativeCompactionItem } from "./checkpoint.js";
6
+
7
+ interface CodexCompactionStreamResult {
8
+ item: unknown;
9
+ responseId?: string;
10
+ responseItems: unknown[];
11
+ }
12
+
13
+ export async function collectCodexCompactionStream(
14
+ events: AsyncIterable<StreamEventShape>,
15
+ ): Promise<CodexCompactionStreamResult> {
16
+ let outputItemCount = 0;
17
+ const compacted: unknown[] = [];
18
+ const responseItems: unknown[] = [];
19
+ let responseId: string | undefined;
20
+ for await (const event of events) {
21
+ if (event.type === "response.created" && event.response?.id) {
22
+ responseId = event.response.id;
23
+ }
24
+ if (event.type === "response.output_item.done" && event.item) {
25
+ outputItemCount++;
26
+ responseItems.push(event.item);
27
+ if (isNativeCompactionItem(event.item)) compacted.push(event.item);
28
+ }
29
+ if (
30
+ (event.type === "response.completed" || event.type === "response.incomplete")
31
+ && event.response?.id
32
+ ) {
33
+ responseId = event.response.id;
34
+ }
35
+ }
36
+ if (compacted.length !== 1) {
37
+ throw new NonRetryableProviderError(
38
+ `OpenAI compaction trigger expected exactly one compaction item, received ${compacted.length} from ${outputItemCount} output items`,
39
+ );
40
+ }
41
+ return {
42
+ item: compacted[0],
43
+ ...(responseId ? { responseId } : {}),
44
+ responseItems,
45
+ };
46
+ }
47
+
48
+ export async function collectCodexCompactionOutput(response: Response, sessionKey?: string): Promise<unknown> {
49
+ const result = await collectCodexCompactionStream(mapCodexEvents(parseSSE(response), sessionKey));
50
+ return result.item;
51
+ }
@@ -0,0 +1,101 @@
1
+ import { BASE_DELAY_MS, CODEX_REMOTE_COMPACTION_STREAM_RETRIES, MAX_RETRIES, SSE_RESPONSE_HEADER_TIMEOUT_MS } from "../../providers/openai-codex/constants.js";
2
+ import { NonRetryableProviderError, isRetryableError, parseErrorResponse, withHttpStatusPrefix } from "../../providers/openai-codex/errors.js";
3
+ import { proxyDispatcherForUrl } from "../../providers/openai-codex/proxy.js";
4
+ import { sleep } from "../../providers/openai-codex/retry.js";
5
+ import { fetchWithResponseHeaderTimeout } from "../../providers/openai-codex/sse.js";
6
+ import { type ResponsesBody } from "@oai404iao/pi-codex-runtime/internal/providers/openai-codex/types";
7
+ import { collectCodexCompactionOutput } from "./collect.js";
8
+
9
+ export async function postJsonWithRetries(
10
+ url: string,
11
+ headers: Headers,
12
+ body: unknown,
13
+ signal: AbortSignal | undefined,
14
+ timeoutMs = SSE_RESPONSE_HEADER_TIMEOUT_MS,
15
+ ): Promise<Record<string, unknown>> {
16
+ const bodyJson = JSON.stringify(body);
17
+ const dispatcher = await proxyDispatcherForUrl(url);
18
+ let lastError: Error | undefined;
19
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
20
+ try {
21
+ const response = await fetchWithResponseHeaderTimeout(url, {
22
+ method: "POST",
23
+ headers,
24
+ body: bodyJson,
25
+ ...(dispatcher ? { dispatcher } : {}),
26
+ } as RequestInit, signal, timeoutMs);
27
+ if (response.ok) {
28
+ const parsed = await response.json();
29
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
30
+ throw new NonRetryableProviderError("OpenAI native compaction returned a non-object response");
31
+ }
32
+ return parsed as Record<string, unknown>;
33
+ }
34
+ const errorText = await response.text();
35
+ if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
36
+ await sleep(BASE_DELAY_MS * 2 ** attempt, signal);
37
+ continue;
38
+ }
39
+ const info = await parseErrorResponse(new Response(errorText, {
40
+ status: response.status,
41
+ statusText: response.statusText,
42
+ }));
43
+ throw new NonRetryableProviderError(withHttpStatusPrefix(response.status, info.friendlyMessage || info.message));
44
+ } catch (error) {
45
+ if (error instanceof NonRetryableProviderError) throw error;
46
+ if (signal?.aborted) throw new Error("Request was aborted");
47
+ lastError = error instanceof Error ? error : new Error(String(error));
48
+ if (attempt < MAX_RETRIES) {
49
+ await sleep(BASE_DELAY_MS * 2 ** attempt, signal);
50
+ continue;
51
+ }
52
+ throw lastError;
53
+ }
54
+ }
55
+ throw lastError ?? new Error("OpenAI native compaction failed");
56
+ }
57
+
58
+ export async function requestCodexCompactionTrigger(
59
+ url: string,
60
+ headers: Headers,
61
+ body: ResponsesBody,
62
+ signal: AbortSignal | undefined,
63
+ sessionKey?: string,
64
+ ): Promise<unknown> {
65
+ const bodyJson = JSON.stringify(body);
66
+ const dispatcher = await proxyDispatcherForUrl(url);
67
+ let lastError: Error | undefined;
68
+ const maxRetries = Math.min(MAX_RETRIES, CODEX_REMOTE_COMPACTION_STREAM_RETRIES);
69
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
70
+ try {
71
+ const response = await fetchWithResponseHeaderTimeout(url, {
72
+ method: "POST",
73
+ headers,
74
+ body: bodyJson,
75
+ ...(dispatcher ? { dispatcher } : {}),
76
+ } as RequestInit, signal);
77
+ if (response.ok) return await collectCodexCompactionOutput(response, sessionKey);
78
+
79
+ const errorText = await response.text();
80
+ if (attempt < maxRetries && isRetryableError(response.status, errorText)) {
81
+ await sleep(BASE_DELAY_MS * 2 ** attempt, signal);
82
+ continue;
83
+ }
84
+ const info = await parseErrorResponse(new Response(errorText, {
85
+ status: response.status,
86
+ statusText: response.statusText,
87
+ }));
88
+ throw new NonRetryableProviderError(withHttpStatusPrefix(response.status, info.friendlyMessage || info.message));
89
+ } catch (error) {
90
+ if (error instanceof NonRetryableProviderError) throw error;
91
+ if (signal?.aborted) throw new Error("Request was aborted");
92
+ lastError = error instanceof Error ? error : new Error(String(error));
93
+ if (attempt < maxRetries) {
94
+ await sleep(BASE_DELAY_MS * 2 ** attempt, signal);
95
+ continue;
96
+ }
97
+ throw lastError;
98
+ }
99
+ }
100
+ throw lastError ?? new Error("OpenAI compaction trigger failed");
101
+ }
@@ -0,0 +1,159 @@
1
+ import { type ProviderHeaders } from "@earendil-works/pi-ai";
2
+ import { type Api, type Context, type Model, type SimpleStreamOptions } from "@earendil-works/pi-ai/compat";
3
+ import { hasCodexRequestAuth, resolveCodexRequestAccountId } from "@oai404iao/pi-codex-runtime/internal/codex-http";
4
+ import { resolveCodexRequestProfile } from "@oai404iao/pi-codex-runtime/internal/codex-request-profile";
5
+ import { resolveCodexRequestIdentity } from "@oai404iao/pi-codex-runtime/internal/codex-wire-identity";
6
+ import { applyFastModeServiceTier } from "../../fast-mode.js";
7
+ import { loadModelSettings, type ResolvedCodexModelSettings } from "@oai404iao/pi-codex-runtime/internal/model-catalog/runtime";
8
+ import { setProviderGeneratedHeader } from "@oai404iao/pi-codex-runtime/internal/provider-headers";
9
+ import { rewriteNativeOpenAiTools } from "../../provider-native-tools.js";
10
+ import { CODEX_COMPACTION_TRIGGER_TYPE, X_OPENAI_INTERNAL_CODEX_RESPONSES_LITE } from "../../providers/openai-codex/constants.js";
11
+ import { applyConfiguredResponsesFeatureHeaders, buildJsonHeaders, buildSSEHeaders, buildWebSocketHeaders } from "../../providers/openai-codex/headers.js";
12
+ import { buildRequestBody, ensureWebSearchDetailsIncluded } from "../../providers/openai-codex/request-body.js";
13
+ import { createCodexRequestId } from "../../providers/openai-codex/request-metadata.js";
14
+ import { compactUrl } from "../../providers/openai-codex/urls.js";
15
+ import { buildCodexCompactionCheckpoint, compactionItems, sanitizeNativeCompactionOutput } from "./checkpoint.js";
16
+ import { postJsonWithRetries } from "./http.js";
17
+ import { requestCodexCompactionTriggerWithTransport } from "./transport.js";
18
+ import type { NativeToolOwnership } from "@oai404iao/pi-codex-runtime/internal/providers/openai-codex/types";
19
+
20
+ export async function requestOpenAINativeCompaction(
21
+ model: Model<Api>,
22
+ context: Context,
23
+ options: {
24
+ ownsNativeTool?: NativeToolOwnership;
25
+ mode: "responses" | "responses-compact";
26
+ apiKey: string;
27
+ headers?: ProviderHeaders;
28
+ signal?: AbortSignal;
29
+ reasoning?: SimpleStreamOptions["reasoning"];
30
+ sessionId?: string;
31
+ turnId?: string;
32
+ maxRetries?: number;
33
+ maxRetryDelayMs?: number;
34
+ settings: ResolvedCodexModelSettings;
35
+ },
36
+ ): Promise<unknown[]> {
37
+ const settings = options.settings.modelProfile
38
+ ? options.settings
39
+ : loadModelSettings(model, undefined, options.settings);
40
+ const auth = { apiKey: options.apiKey || undefined, headers: options.headers };
41
+ if (!hasCodexRequestAuth({ modelHeaders: model.headers, auth })) {
42
+ throw new Error(`No request authentication for provider: ${model.provider}`);
43
+ }
44
+ if (settings.compactionMode === "pi") {
45
+ throw new Error("native compaction is disabled by the current model profile");
46
+ }
47
+ const profile = resolveCodexRequestProfile(settings.requestProfile);
48
+ const accountId = resolveCodexRequestAccountId({
49
+ modelHeaders: model.headers,
50
+ auth,
51
+ apiKeyMode: settings.apiKeyMode,
52
+ });
53
+ const requestIdentity = resolveCodexRequestIdentity(
54
+ options.sessionId,
55
+ options.turnId ? { turn_id: options.turnId } : undefined,
56
+ "compaction",
57
+ );
58
+ let body = applyFastModeServiceTier(buildRequestBody(model, context, profile, {
59
+ ownsNativeTool: options.ownsNativeTool,
60
+ apiKey: options.apiKey,
61
+ headers: options.headers,
62
+ signal: options.signal,
63
+ reasoning: options.reasoning,
64
+ sessionId: options.sessionId,
65
+ }), settings, model);
66
+ if (settings.nativeProviderTools) {
67
+ const webSearch = settings.modelProfile?.effective.tools.webSearch;
68
+ body = rewriteNativeOpenAiTools(body, {
69
+ ownsNativeTool: options.ownsNativeTool,
70
+ imageModel: settings.imageModel,
71
+ imageGeneration: settings.imageGenerationImplementation ?? false,
72
+ webSearch: settings.webSearchEnabled
73
+ && webSearch
74
+ ? {
75
+ implementation: webSearch.implementation,
76
+ contentTypes: webSearch.contentTypes,
77
+ }
78
+ : false,
79
+ }).payload;
80
+ }
81
+ ensureWebSearchDetailsIncluded(body);
82
+
83
+ if (options.mode === "responses") {
84
+ const retainedInput = [...body.input];
85
+ body.input = [...retainedInput, { type: CODEX_COMPACTION_TRIGGER_TYPE }];
86
+ const sseHeaders = applyConfiguredResponsesFeatureHeaders(buildSSEHeaders(
87
+ model.headers,
88
+ options.headers,
89
+ accountId,
90
+ options.apiKey,
91
+ options.sessionId,
92
+ profile,
93
+ requestIdentity?.threadId,
94
+ requestIdentity,
95
+ ), settings, model);
96
+ const requestId = requestIdentity?.threadId
97
+ ?? options.sessionId
98
+ ?? createCodexRequestId();
99
+ const websocketHeaders = applyConfiguredResponsesFeatureHeaders(buildWebSocketHeaders(
100
+ model.headers,
101
+ options.headers,
102
+ accountId,
103
+ options.apiKey,
104
+ requestId,
105
+ requestId,
106
+ requestIdentity,
107
+ ), settings, model);
108
+ const item = await requestCodexCompactionTriggerWithTransport(
109
+ model,
110
+ { sse: sseHeaders, websocket: websocketHeaders },
111
+ body,
112
+ {
113
+ sessionId: options.sessionId,
114
+ turnId: options.turnId,
115
+ requestIdentity,
116
+ signal: options.signal,
117
+ settings,
118
+ maxRetries: options.maxRetries,
119
+ maxRetryDelayMs: options.maxRetryDelayMs,
120
+ },
121
+ );
122
+ return buildCodexCompactionCheckpoint(retainedInput, item);
123
+ }
124
+
125
+ const headers = buildJsonHeaders(
126
+ model.headers,
127
+ options.headers,
128
+ accountId,
129
+ options.apiKey,
130
+ options.sessionId,
131
+ requestIdentity,
132
+ );
133
+ if (profile.responsesMode === "lite") {
134
+ setProviderGeneratedHeader(headers, X_OPENAI_INTERNAL_CODEX_RESPONSES_LITE, "true");
135
+ }
136
+ const compactBody: Record<string, unknown> = {
137
+ model: body.model,
138
+ input: body.input,
139
+ parallel_tool_calls: body.parallel_tool_calls,
140
+ };
141
+ for (const key of ["instructions", "tools", "reasoning", "service_tier", "prompt_cache_key", "text"] as const) {
142
+ if (body[key] !== undefined) compactBody[key] = body[key];
143
+ }
144
+ const response = await postJsonWithRetries(
145
+ compactUrl(model.baseUrl, settings.apiKeyMode),
146
+ headers,
147
+ compactBody,
148
+ options.signal,
149
+ );
150
+ const output = response.output;
151
+ if (!Array.isArray(output) || output.length === 0) {
152
+ throw new Error("OpenAI /responses/compact returned no replacement output");
153
+ }
154
+ const sanitizedOutput = sanitizeNativeCompactionOutput(output);
155
+ if (compactionItems(sanitizedOutput).length === 0) {
156
+ throw new Error("OpenAI /responses/compact output did not contain a compaction item");
157
+ }
158
+ return sanitizedOutput;
159
+ }
@@ -0,0 +1,125 @@
1
+ import { type Api, type Model, type SimpleStreamOptions } from "@earendil-works/pi-ai/compat";
2
+ import { resolveCodexRequestProfile } from "@oai404iao/pi-codex-runtime/internal/codex-request-profile";
3
+ import { type CodexRequestIdentity } from "@oai404iao/pi-codex-runtime/internal/codex-wire-identity";
4
+ import { type ResolvedCodexModelSettings } from "@oai404iao/pi-codex-runtime/internal/model-catalog/runtime";
5
+ import { webSocketFallbackKey } from "../../providers/openai-codex/cache-key.js";
6
+ import { CODEX_REMOTE_COMPACTION_STREAM_RETRIES } from "../../providers/openai-codex/constants.js";
7
+ import { withResponsesLiteWebSocketMetadata } from "../../providers/openai-codex/lite.js";
8
+ import { createPiTurnId, withSseRequestMetadata } from "../../providers/openai-codex/request-metadata.js";
9
+ import { isRetryableWebSocketError, isWebSocketConnectionLimitReachedError, isWebSocketUpgradeRejectedError, sleep, webSocketCompactionRetryDelayMs, webSocketStreamMaxRetries } from "../../providers/openai-codex/retry.js";
10
+ import { type ResponsesBody, type WebSocketRequestMetadata } from "@oai404iao/pi-codex-runtime/internal/providers/openai-codex/types";
11
+ import { resolveCodexUrl, resolveResponsesWebSocketUrl } from "../../providers/openai-codex/urls.js";
12
+ import { websocketHttpFallbackSessions } from "../../providers/openai-codex/websocket-session.js";
13
+ import { requestCodexCompactionTrigger } from "./http.js";
14
+ import { requestCodexCompactionTriggerWebSocket } from "./websocket.js";
15
+
16
+ export async function requestCodexCompactionTriggerWithTransport(
17
+ model: Model<Api>,
18
+ headers: {
19
+ sse: Headers;
20
+ websocket: Headers;
21
+ },
22
+ body: ResponsesBody,
23
+ options: {
24
+ sessionId?: string;
25
+ turnId?: string;
26
+ requestIdentity?: CodexRequestIdentity;
27
+ signal?: AbortSignal;
28
+ settings: ResolvedCodexModelSettings;
29
+ maxRetries?: number;
30
+ maxRetryDelayMs?: number;
31
+ },
32
+ ): Promise<unknown> {
33
+ const transport = options.settings.openaiTransport;
34
+ const responsesMode = resolveCodexRequestProfile(options.settings.requestProfile).responsesMode;
35
+ const sseUrl = resolveCodexUrl(model.baseUrl, { apiKeyMode: options.settings.apiKeyMode });
36
+ const requestMetadata: WebSocketRequestMetadata = {
37
+ ...(options.sessionId ? { sessionId: options.sessionId } : {}),
38
+ ...(options.requestIdentity?.threadId
39
+ ? { threadId: options.requestIdentity.threadId }
40
+ : {}),
41
+ turnId: options.requestIdentity?.turnId
42
+ || options.turnId
43
+ || createPiTurnId(),
44
+ requestKind: "compaction",
45
+ ...(options.requestIdentity
46
+ ? { identity: options.requestIdentity }
47
+ : {}),
48
+ };
49
+ if (transport === "sse") {
50
+ return requestCodexCompactionTrigger(
51
+ sseUrl,
52
+ headers.sse,
53
+ withSseRequestMetadata(body, requestMetadata),
54
+ options.signal,
55
+ options.sessionId,
56
+ );
57
+ }
58
+
59
+ const websocketUrl = resolveResponsesWebSocketUrl(model.baseUrl, { apiKeyMode: options.settings.apiKeyMode });
60
+ const fallbackKey = webSocketFallbackKey(
61
+ options.sessionId,
62
+ model,
63
+ websocketUrl,
64
+ options.settings.modelProfileHash,
65
+ );
66
+ if (
67
+ transport === "auto"
68
+ && fallbackKey
69
+ && websocketHttpFallbackSessions.has(fallbackKey)
70
+ ) {
71
+ return requestCodexCompactionTrigger(
72
+ sseUrl,
73
+ headers.sse,
74
+ withSseRequestMetadata(body, requestMetadata),
75
+ options.signal,
76
+ options.sessionId,
77
+ );
78
+ }
79
+
80
+ const maxRetries = Math.min(
81
+ CODEX_REMOTE_COMPACTION_STREAM_RETRIES,
82
+ webSocketStreamMaxRetries({
83
+ maxRetries: options.maxRetries,
84
+ } as SimpleStreamOptions),
85
+ );
86
+ const retryOptions = {
87
+ ...(options.maxRetryDelayMs !== undefined ? { maxRetryDelayMs: options.maxRetryDelayMs } : {}),
88
+ } as SimpleStreamOptions;
89
+ let retries = 0;
90
+ while (true) {
91
+ try {
92
+ return await requestCodexCompactionTriggerWebSocket(
93
+ websocketUrl,
94
+ headers.websocket,
95
+ withResponsesLiteWebSocketMetadata(body, responsesMode),
96
+ model,
97
+ requestMetadata,
98
+ options.signal,
99
+ options.settings.modelProfileHash,
100
+ );
101
+ } catch (error) {
102
+ if (options.signal?.aborted) throw new Error("Request was aborted");
103
+ const upgradeRejected = isWebSocketUpgradeRejectedError(error);
104
+ if (transport === "auto" && upgradeRejected) {
105
+ if (fallbackKey) websocketHttpFallbackSessions.add(fallbackKey);
106
+ break;
107
+ }
108
+ const retryable = isWebSocketConnectionLimitReachedError(error) || isRetryableWebSocketError(error);
109
+ if (retryable && retries < maxRetries) {
110
+ retries++;
111
+ await sleep(webSocketCompactionRetryDelayMs(error, retries, retryOptions), options.signal);
112
+ continue;
113
+ }
114
+ throw error;
115
+ }
116
+ }
117
+
118
+ return requestCodexCompactionTrigger(
119
+ sseUrl,
120
+ headers.sse,
121
+ withSseRequestMetadata(body, requestMetadata),
122
+ options.signal,
123
+ options.sessionId,
124
+ );
125
+ }
@@ -0,0 +1,119 @@
1
+ import { type Api, type Model } from "@earendil-works/pi-ai/compat";
2
+ import { webSocketCacheKey } from "../../providers/openai-codex/cache-key.js";
3
+ import { WEBSOCKET_CONNECT_TIMEOUT_MS, WEBSOCKET_SEND_TIMEOUT_MS } from "../../providers/openai-codex/constants.js";
4
+ import { buildCachedWebSocketRequestBody, prepareWebSocketRequestBodyForWire } from "../../providers/openai-codex/continuation.js";
5
+ import { mapCodexEvents } from "../../providers/openai-codex/events.js";
6
+ import { withWebSocketRequestMetadata } from "../../providers/openai-codex/request-metadata.js";
7
+ import { isPreviousResponseNotFoundError, isRetryableEarlyWebSocketError } from "../../providers/openai-codex/retry.js";
8
+ import { type ResponsesBody, type WebSocketRequestMetadata } from "@oai404iao/pi-codex-runtime/internal/providers/openai-codex/types";
9
+ import { countWebSocketEvents, parseWebSocket, sendWebSocketRequest } from "../../providers/openai-codex/websocket-events.js";
10
+ import { acquireWebSocket } from "../../providers/openai-codex/websocket-session.js";
11
+ import { collectCodexCompactionStream } from "./collect.js";
12
+
13
+ export async function requestCodexCompactionTriggerWebSocket(
14
+ url: string,
15
+ headers: Headers,
16
+ body: ResponsesBody,
17
+ model: Model<Api>,
18
+ requestMetadata: WebSocketRequestMetadata,
19
+ signal: AbortSignal | undefined,
20
+ profileHash?: string,
21
+ ): Promise<unknown> {
22
+ let disableCachedContext = false;
23
+ let staleSocketRetried = false;
24
+ let missingPreviousResponseRetried = false;
25
+
26
+ while (true) {
27
+ const cacheKey = webSocketCacheKey(
28
+ requestMetadata.sessionId,
29
+ model,
30
+ url,
31
+ headers,
32
+ profileHash,
33
+ );
34
+ const { socket, entry, release, reused } = await acquireWebSocket(
35
+ url,
36
+ headers,
37
+ cacheKey,
38
+ requestMetadata.sessionId,
39
+ signal,
40
+ WEBSOCKET_CONNECT_TIMEOUT_MS,
41
+ );
42
+ let keepConnection = true;
43
+ let released = false;
44
+ let eventCount = 0;
45
+ // All reusable WebSocket transports opportunistically continue an exact
46
+ // logical request prefix. `websocket` still sends a full request whenever
47
+ // the stable fields or input prefix do not match.
48
+ const useCachedContext = true;
49
+ const fullBody = withWebSocketRequestMetadata(body, requestMetadata);
50
+ const requestBody = useCachedContext && !disableCachedContext && entry
51
+ ? buildCachedWebSocketRequestBody(entry, fullBody)
52
+ : fullBody;
53
+ const wireRequestBody = prepareWebSocketRequestBodyForWire(requestBody);
54
+ const releaseOnce = (releaseOptions?: { keep?: boolean }) => {
55
+ if (released) return;
56
+ released = true;
57
+ release(releaseOptions);
58
+ };
59
+
60
+ try {
61
+ await sendWebSocketRequest(
62
+ socket,
63
+ JSON.stringify({ type: "response.create", ...wireRequestBody }),
64
+ signal,
65
+ WEBSOCKET_SEND_TIMEOUT_MS,
66
+ );
67
+ const result = await collectCodexCompactionStream(
68
+ mapCodexEvents(
69
+ countWebSocketEvents(parseWebSocket(socket, signal), () => {
70
+ eventCount++;
71
+ }),
72
+ requestMetadata.sessionId,
73
+ ),
74
+ );
75
+ if (signal?.aborted) {
76
+ keepConnection = false;
77
+ throw new Error("Request was aborted");
78
+ }
79
+ if (entry && result.responseId) {
80
+ entry.continuation = {
81
+ lastRequestBody: fullBody,
82
+ lastResponseId: result.responseId,
83
+ lastResponseItems: result.responseItems,
84
+ };
85
+ } else if (entry) {
86
+ entry.continuation = undefined;
87
+ }
88
+ releaseOnce({ keep: true });
89
+ return result.item;
90
+ } catch (error) {
91
+ if (entry) entry.continuation = undefined;
92
+ keepConnection = false;
93
+ releaseOnce({ keep: false });
94
+ if (
95
+ !staleSocketRetried
96
+ && reused
97
+ && eventCount === 0
98
+ && !signal?.aborted
99
+ && isRetryableEarlyWebSocketError(error)
100
+ ) {
101
+ staleSocketRetried = true;
102
+ continue;
103
+ }
104
+ if (
105
+ !missingPreviousResponseRetried
106
+ && requestBody.previous_response_id
107
+ && !signal?.aborted
108
+ && isPreviousResponseNotFoundError(error)
109
+ ) {
110
+ missingPreviousResponseRetried = true;
111
+ disableCachedContext = true;
112
+ continue;
113
+ }
114
+ throw error;
115
+ } finally {
116
+ releaseOnce({ keep: keepConnection });
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,27 @@
1
+ import { type Context, type ThinkingLevel } from "@earendil-works/pi-ai/compat";
2
+ import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+ import { thinkingLevelFromUnknown } from "../providers/openai-codex/reasoning.js";
4
+
5
+ export interface StartupPrewarmSnapshot {
6
+ systemPrompt: string;
7
+ tools: Context["tools"];
8
+ reasoning?: ThinkingLevel;
9
+ }
10
+
11
+ export function startupPrewarmSnapshot(pi: ExtensionAPI, ctx: any): StartupPrewarmSnapshot {
12
+ const activeToolNames = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [];
13
+ return {
14
+ systemPrompt: ctx.getSystemPrompt?.() ?? "",
15
+ tools: (typeof pi.getAllTools === "function" ? pi.getAllTools() : [])
16
+ .filter((tool) => activeToolNames.includes(tool.name))
17
+ .map((tool) => ({
18
+ name: tool.name,
19
+ description: tool.description,
20
+ parameters: tool.parameters,
21
+ })),
22
+ reasoning: thinkingLevelFromUnknown(
23
+ (ctx as { thinkingLevel?: unknown }).thinkingLevel
24
+ ?? (typeof pi.getThinkingLevel === "function" ? pi.getThinkingLevel() : undefined),
25
+ ),
26
+ };
27
+ }
@@ -0,0 +1,101 @@
1
+ import {
2
+ streamSimpleOpenAICodexResponses, streamSimpleOpenAIResponses,
3
+ type Api, type Context, type Model, type SimpleStreamOptions,
4
+ } from "@earendil-works/pi-ai/compat";
5
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
+ import { installCodexIdentityLifecycle } from "@oai404iao/pi-codex-runtime/internal/codex-identity-extension";
7
+ import { currentCodexTurn, resolveCodexRequestIdentity } from "@oai404iao/pi-codex-runtime/internal/codex-wire-identity";
8
+ import { loadModelSettings } from "@oai404iao/pi-codex-runtime/internal/model-catalog/runtime";
9
+ import { createCodexStream } from "../providers/openai-codex/stream.js";
10
+ import type { OpenAIResponsesProviderController } from "@oai404iao/pi-codex-runtime/internal/providers/openai-codex/types";
11
+ import { closeProviderWebSocketSessions } from "../providers/openai-codex/websocket-session.js";
12
+ import type { ProviderPresentation } from "@oai404iao/pi-codex-runtime/internal/extension/provider-presentation";
13
+ import { createStartupPrewarmLifecycle } from "./startup-prewarm.js";
14
+
15
+ export function registerResponsesProviderRuntime(
16
+ pi: ExtensionAPI,
17
+ options: { getCurrentCwd: () => string; ownsNativeTool?: OpenAIResponsesProviderController["ownsNativeTool"] },
18
+ presentation?: ProviderPresentation,
19
+ ): OpenAIResponsesProviderController {
20
+ installCodexIdentityLifecycle(pi);
21
+ const prewarm = createStartupPrewarmLifecycle(pi, options.ownsNativeTool);
22
+ const streamSimple = <TApi extends Api>(model: Model<TApi>, context: Context, streamOptions?: SimpleStreamOptions) => {
23
+ const settings = loadModelSettings(model, options.getCurrentCwd());
24
+ if (
25
+ !settings.enabled
26
+ || !settings.modelProfile?.effective.enabled
27
+ || !settings.providerShimActive
28
+ ) {
29
+ return model.api === "openai-codex-responses"
30
+ ? streamSimpleOpenAICodexResponses(model as Model<"openai-codex-responses">, context, streamOptions)
31
+ : streamSimpleOpenAIResponses(model as Model<"openai-responses">, context, streamOptions);
32
+ }
33
+ return createCodexStream(model, context, streamOptions, {
34
+ ownsNativeTool: options.ownsNativeTool,
35
+ ...presentation?.streamEffects(),
36
+ getCurrentCwd: options.getCurrentCwd,
37
+ getCurrentTurnId: (sessionId) => currentCodexTurn(sessionId)?.turnId,
38
+ getStartupPrewarm: prewarm.get,
39
+ });
40
+ };
41
+
42
+ type CodexResponsesApi = "openai-responses" | "openai-codex-responses";
43
+ const registeredProviderApis = new Map<string, CodexResponsesApi>();
44
+ const registerProviderShim = (provider: string, api: CodexResponsesApi): void => {
45
+ if (!provider || registeredProviderApis.get(provider) === api) return;
46
+ pi.registerProvider(provider, { api, streamSimple });
47
+ registeredProviderApis.set(provider, api);
48
+ };
49
+ const ensureProviderShimForModel = (model: Model<Api> | undefined, cwd?: string): void => {
50
+ if (!model) return;
51
+ const settings = loadModelSettings(model, cwd);
52
+ if (
53
+ !settings.enabled
54
+ || !settings.modelProfile?.effective.enabled
55
+ || !settings.providerShimActive
56
+ ) {
57
+ return;
58
+ }
59
+ if (model.api === "openai-responses" || model.api === "openai-codex-responses") {
60
+ registerProviderShim(model.provider, model.api as CodexResponsesApi);
61
+ }
62
+ };
63
+
64
+ // Register built-ins first; custom providers require an actual selected
65
+ // model so their URL, auth and model list are never created or overwritten.
66
+ registerProviderShim("openai-codex", "openai-codex-responses");
67
+ registerProviderShim("openai", "openai-responses");
68
+
69
+ pi.on("session_start", async (_event, ctx) => {
70
+ prewarm.reset();
71
+ ensureProviderShimForModel(ctx?.model as Model<Api> | undefined, ctx?.cwd);
72
+ presentation?.clear();
73
+ prewarm.start(ctx);
74
+ });
75
+ pi.on("model_select", async (_event, ctx) => {
76
+ ensureProviderShimForModel(ctx?.model as Model<Api> | undefined, ctx?.cwd);
77
+ });
78
+ pi.on("session_shutdown", async (_event, ctx) => {
79
+ prewarm.reset();
80
+ presentation?.flush();
81
+ closeProviderWebSocketSessions(ctx?.sessionManager?.getSessionId?.());
82
+ presentation?.clear();
83
+ });
84
+ pi.on("before_agent_start", async (_event, ctx) => {
85
+ ensureProviderShimForModel(ctx.model as Model<Api> | undefined, ctx.cwd);
86
+ });
87
+ pi.on("agent_end", async () => {
88
+ presentation?.scheduleFlush();
89
+ });
90
+ presentation?.registerRenderers();
91
+
92
+ return {
93
+ ownsNativeTool: options.ownsNativeTool,
94
+ getCurrentTurnId(sessionId) {
95
+ return currentCodexTurn(sessionId)?.turnId;
96
+ },
97
+ getRequestIdentity(sessionId, requestKind = "turn") {
98
+ return resolveCodexRequestIdentity(sessionId, undefined, requestKind);
99
+ },
100
+ };
101
+ }