@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.
- package/LICENSE +28 -0
- package/LICENSES/Apache-2.0.txt +201 -0
- package/LICENSES/OpenAI-Codex-NOTICE.txt +6 -0
- package/README.md +26 -0
- package/THIRD_PARTY_NOTICES.md +18 -0
- package/package.json +84 -0
- package/provenance/openai-codex-eb9dceba-reserved-tools.json +140 -0
- package/src/adapter/compaction/checkpoint.ts +159 -0
- package/src/adapter/compaction/collect.ts +51 -0
- package/src/adapter/compaction/http.ts +101 -0
- package/src/adapter/compaction/request.ts +159 -0
- package/src/adapter/compaction/transport.ts +125 -0
- package/src/adapter/compaction/websocket.ts +119 -0
- package/src/extension/prewarm-snapshot.ts +27 -0
- package/src/extension/provider-runtime.ts +101 -0
- package/src/extension/startup-prewarm.ts +264 -0
- package/src/fast-mode.ts +124 -0
- package/src/index.ts +257 -0
- package/src/native-compaction.ts +392 -0
- package/src/patch/apply.ts +338 -0
- package/src/patch/parser.ts +224 -0
- package/src/patch/render.ts +201 -0
- package/src/provider-native-tools.ts +75 -0
- package/src/providers/codex-apply-patch-tool.ts +23 -0
- package/src/providers/codex-apply-patch.lark +19 -0
- package/src/providers/openai-codex/cache-key.ts +52 -0
- package/src/providers/openai-codex/captured-stream.ts +50 -0
- package/src/providers/openai-codex/constants.ts +61 -0
- package/src/providers/openai-codex/continuation.ts +110 -0
- package/src/providers/openai-codex/errors.ts +130 -0
- package/src/providers/openai-codex/events.ts +123 -0
- package/src/providers/openai-codex/headers.ts +224 -0
- package/src/providers/openai-codex/lite.ts +24 -0
- package/src/providers/openai-codex/message.ts +33 -0
- package/src/providers/openai-codex/prewarm.ts +76 -0
- package/src/providers/openai-codex/proxy.ts +55 -0
- package/src/providers/openai-codex/reasoning.ts +54 -0
- package/src/providers/openai-codex/request-body.ts +149 -0
- package/src/providers/openai-codex/request-context.ts +20 -0
- package/src/providers/openai-codex/request-metadata.ts +137 -0
- package/src/providers/openai-codex/retry.ts +154 -0
- package/src/providers/openai-codex/runtime.ts +1 -0
- package/src/providers/openai-codex/sse.ts +93 -0
- package/src/providers/openai-codex/stream.ts +367 -0
- package/src/providers/openai-codex/urls.ts +24 -0
- package/src/providers/openai-codex/usage.ts +60 -0
- package/src/providers/openai-codex/websocket-connection.ts +216 -0
- package/src/providers/openai-codex/websocket-events.ts +210 -0
- package/src/providers/openai-codex/websocket-session.ts +192 -0
- package/src/providers/openai-codex/websocket-socket.ts +18 -0
- package/src/providers/openai-codex/websocket-stream.ts +151 -0
- package/src/tools/apply-patch.ts +84 -0
- 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
|
+
}
|