@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,367 @@
|
|
|
1
|
+
import { type ProviderHeaders } from "@earendil-works/pi-ai";
|
|
2
|
+
import { appendAssistantMessageDiagnostic, createAssistantMessageDiagnostic, createAssistantMessageEventStream, getEnvApiKey, type Api, type AssistantMessageEventStream, 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 { captureCodexTurnState, resolveCodexRequestIdentity } from "@oai404iao/pi-codex-runtime/internal/codex-wire-identity";
|
|
6
|
+
import { applyFastModeServiceTier } from "../../fast-mode.js";
|
|
7
|
+
import { loadModelSettings } from "@oai404iao/pi-codex-runtime/internal/model-catalog/runtime";
|
|
8
|
+
import { rewriteNativeOpenAiTools, type NativeToolRewriteOptions } from "../../provider-native-tools.js";
|
|
9
|
+
import { collectHistoricalCitationSources, collectWebSearchCitationSources } from "@oai404iao/pi-codex-runtime/internal/providers/responses/citations";
|
|
10
|
+
import { webSocketFallbackKey } from "./cache-key.js";
|
|
11
|
+
import { processCapturedResponsesStream } from "./captured-stream.js";
|
|
12
|
+
import type { ProviderStreamEffects } from "@oai404iao/pi-codex-runtime/internal/providers/openai-codex/stream-effects";
|
|
13
|
+
import { BASE_DELAY_MS, MAX_RETRIES } from "./constants.js";
|
|
14
|
+
import { NonRetryableProviderError, isRetryableError, parseErrorResponse, withHttpStatusPrefix } from "./errors.js";
|
|
15
|
+
import { applyConfiguredResponsesFeatureHeaders, buildSSEHeaders, buildWebSocketHeaders, headersToRecord, providerHeadersToHeaders } from "./headers.js";
|
|
16
|
+
import { withResponsesLiteWebSocketMetadata } from "./lite.js";
|
|
17
|
+
import { createErrorMessage, createInitialAssistantMessage } from "./message.js";
|
|
18
|
+
import { proxyDispatcherForUrl } from "./proxy.js";
|
|
19
|
+
import { buildRequestBody, ensureWebSearchDetailsIncluded } from "./request-body.js";
|
|
20
|
+
import { getLatestUserText } from "./request-context.js";
|
|
21
|
+
import { createCodexRequestId, createPiTurnId, withSseRequestMetadata } from "./request-metadata.js";
|
|
22
|
+
import { isProviderNonTransportError, isRetryableWebSocketError, isWebSocketConnectionLimitReachedError, isWebSocketUpgradeRejectedError, sleep, webSocketRetryDelayMs, webSocketStreamMaxRetries } from "./retry.js";
|
|
23
|
+
import { fetchWithResponseHeaderTimeout, parseSSE, responseHeaderTimeoutMsFromOptions } from "./sse.js";
|
|
24
|
+
import { type ProviderTransport, type ResponsesBody, type WebSocketRequestMetadata } from "@oai404iao/pi-codex-runtime/internal/providers/openai-codex/types";
|
|
25
|
+
import { resolveCodexUrl, resolveResponsesWebSocketUrl } from "./urls.js";
|
|
26
|
+
import { finalizeUsage, withRequestServiceTier } from "./usage.js";
|
|
27
|
+
import { websocketHttpFallbackSessions } from "./websocket-session.js";
|
|
28
|
+
import { processWebSocketStream } from "./websocket-stream.js";
|
|
29
|
+
|
|
30
|
+
export function createCodexStream<TApi extends Api>(
|
|
31
|
+
model: Model<TApi>,
|
|
32
|
+
context: Context,
|
|
33
|
+
options: SimpleStreamOptions | undefined,
|
|
34
|
+
deps: ProviderStreamEffects & Pick<NativeToolRewriteOptions, "ownsNativeTool"> & {
|
|
35
|
+
getCurrentCwd: () => string;
|
|
36
|
+
getCurrentTurnId?: (sessionId: string | undefined) => string | undefined;
|
|
37
|
+
getStartupPrewarm?: (sessionId: string, model: Model<Api>) => Promise<void> | undefined;
|
|
38
|
+
},
|
|
39
|
+
): AssistantMessageEventStream {
|
|
40
|
+
const stream = createAssistantMessageEventStream();
|
|
41
|
+
const requestCwd = deps.getCurrentCwd();
|
|
42
|
+
|
|
43
|
+
(async () => {
|
|
44
|
+
const output = createInitialAssistantMessage(model);
|
|
45
|
+
const requestPrompt = getLatestUserText(context);
|
|
46
|
+
const webSearchCitationSources = collectWebSearchCitationSources(model, context);
|
|
47
|
+
const historicalCitationSources = collectHistoricalCitationSources(model, context);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
51
|
+
const requestHeaders = options?.headers;
|
|
52
|
+
const auth = { apiKey: apiKey || undefined, headers: requestHeaders };
|
|
53
|
+
if (!hasCodexRequestAuth({ modelHeaders: model.headers, auth })) {
|
|
54
|
+
throw new Error(`No request authentication for provider: ${model.provider}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const settings = loadModelSettings(model, requestCwd);
|
|
58
|
+
const requestProfile = resolveCodexRequestProfile(settings.requestProfile);
|
|
59
|
+
if (
|
|
60
|
+
!settings.enabled
|
|
61
|
+
|| !settings.modelProfile?.effective.enabled
|
|
62
|
+
|| !settings.providerShimActive
|
|
63
|
+
) {
|
|
64
|
+
throw new Error(`No enabled Codex model profile for ${model.provider}/${model.id}`);
|
|
65
|
+
}
|
|
66
|
+
const apiKeyTransport = settings.apiKeyMode;
|
|
67
|
+
const accountId = resolveCodexRequestAccountId({
|
|
68
|
+
modelHeaders: model.headers,
|
|
69
|
+
auth,
|
|
70
|
+
apiKeyMode: apiKeyTransport,
|
|
71
|
+
});
|
|
72
|
+
const requestIdentity = resolveCodexRequestIdentity(
|
|
73
|
+
options?.sessionId,
|
|
74
|
+
options?.metadata as Record<string, unknown> | undefined,
|
|
75
|
+
"turn",
|
|
76
|
+
);
|
|
77
|
+
let body = applyFastModeServiceTier(
|
|
78
|
+
buildRequestBody(model, context, requestProfile, { ...options, ownsNativeTool: deps.ownsNativeTool }),
|
|
79
|
+
settings,
|
|
80
|
+
model,
|
|
81
|
+
);
|
|
82
|
+
if (settings.nativeProviderTools) {
|
|
83
|
+
const webSearch = settings.modelProfile.effective.tools.webSearch;
|
|
84
|
+
body = rewriteNativeOpenAiTools(body, {
|
|
85
|
+
ownsNativeTool: deps.ownsNativeTool,
|
|
86
|
+
imageModel: settings.imageModel,
|
|
87
|
+
imageGeneration: settings.imageGenerationImplementation ?? false,
|
|
88
|
+
webSearch: settings.webSearchEnabled && webSearch
|
|
89
|
+
? {
|
|
90
|
+
implementation: webSearch.implementation,
|
|
91
|
+
contentTypes: webSearch.contentTypes,
|
|
92
|
+
}
|
|
93
|
+
: false,
|
|
94
|
+
}).payload;
|
|
95
|
+
}
|
|
96
|
+
const nextBody = await options?.onPayload?.(body, model);
|
|
97
|
+
if (nextBody !== undefined) {
|
|
98
|
+
body = nextBody as ResponsesBody;
|
|
99
|
+
}
|
|
100
|
+
options = withRequestServiceTier(options, body.service_tier);
|
|
101
|
+
ensureWebSearchDetailsIncluded(body);
|
|
102
|
+
|
|
103
|
+
const websocketSessionId = requestIdentity?.sessionId ?? options?.sessionId;
|
|
104
|
+
const websocketThreadId = requestIdentity?.threadId ?? options?.sessionId;
|
|
105
|
+
const websocketTurnId = requestIdentity?.turnId
|
|
106
|
+
|| deps.getCurrentTurnId?.(options?.sessionId)
|
|
107
|
+
|| createPiTurnId();
|
|
108
|
+
const websocketRequestId = websocketThreadId
|
|
109
|
+
|| websocketSessionId
|
|
110
|
+
|| createCodexRequestId();
|
|
111
|
+
const websocketRequestMetadata: WebSocketRequestMetadata = {
|
|
112
|
+
...(options?.sessionId ? { sessionId: options.sessionId } : {}),
|
|
113
|
+
...(websocketThreadId ? { threadId: websocketThreadId } : {}),
|
|
114
|
+
turnId: websocketTurnId,
|
|
115
|
+
...(requestIdentity ? { identity: requestIdentity } : {}),
|
|
116
|
+
};
|
|
117
|
+
let sseHeaders = applyConfiguredResponsesFeatureHeaders(
|
|
118
|
+
buildSSEHeaders(
|
|
119
|
+
model.headers,
|
|
120
|
+
requestHeaders,
|
|
121
|
+
accountId,
|
|
122
|
+
apiKey,
|
|
123
|
+
options?.sessionId,
|
|
124
|
+
requestProfile,
|
|
125
|
+
websocketThreadId,
|
|
126
|
+
requestIdentity,
|
|
127
|
+
),
|
|
128
|
+
settings,
|
|
129
|
+
model as Model<Api>,
|
|
130
|
+
);
|
|
131
|
+
let websocketHeaders = applyConfiguredResponsesFeatureHeaders(buildWebSocketHeaders(
|
|
132
|
+
model.headers,
|
|
133
|
+
requestHeaders,
|
|
134
|
+
accountId,
|
|
135
|
+
apiKey,
|
|
136
|
+
options?.sessionId ?? websocketRequestId,
|
|
137
|
+
websocketThreadId ?? websocketRequestId,
|
|
138
|
+
requestIdentity,
|
|
139
|
+
), settings, model as Model<Api>);
|
|
140
|
+
const transformHeaders = (
|
|
141
|
+
options as
|
|
142
|
+
| (SimpleStreamOptions & {
|
|
143
|
+
transformHeaders?: (
|
|
144
|
+
headers: ProviderHeaders,
|
|
145
|
+
) => ProviderHeaders | Promise<ProviderHeaders>;
|
|
146
|
+
})
|
|
147
|
+
| undefined
|
|
148
|
+
)?.transformHeaders;
|
|
149
|
+
const bodyJson = JSON.stringify(withSseRequestMetadata(body, websocketRequestMetadata));
|
|
150
|
+
const responseHeaderTimeoutMs = responseHeaderTimeoutMsFromOptions(options);
|
|
151
|
+
const configuredTransport: ProviderTransport = settings.openaiTransport;
|
|
152
|
+
// Pi exposes a session transport setting through stream options. Treat
|
|
153
|
+
// explicit non-auto values as overrides; otherwise use the model profile.
|
|
154
|
+
const transport: ProviderTransport = options?.transport && options.transport !== "auto"
|
|
155
|
+
? options.transport
|
|
156
|
+
: configuredTransport;
|
|
157
|
+
|
|
158
|
+
const websocketUrl = resolveResponsesWebSocketUrl(model.baseUrl, { apiKeyMode: apiKeyTransport });
|
|
159
|
+
const fallbackKey = webSocketFallbackKey(
|
|
160
|
+
options?.sessionId,
|
|
161
|
+
model as Model<Api>,
|
|
162
|
+
websocketUrl,
|
|
163
|
+
settings.modelProfileHash,
|
|
164
|
+
);
|
|
165
|
+
const sessionFellBackToHttp = transport === "auto"
|
|
166
|
+
&& fallbackKey !== undefined
|
|
167
|
+
&& websocketHttpFallbackSessions.has(fallbackKey);
|
|
168
|
+
|
|
169
|
+
if (transport !== "sse" && !sessionFellBackToHttp) {
|
|
170
|
+
if (transformHeaders) {
|
|
171
|
+
websocketHeaders = providerHeadersToHeaders(
|
|
172
|
+
await transformHeaders(
|
|
173
|
+
headersToRecord(websocketHeaders),
|
|
174
|
+
),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
const startupPrewarmTask = options?.sessionId
|
|
178
|
+
? deps.getStartupPrewarm?.(options.sessionId, model as Model<Api>)
|
|
179
|
+
: undefined;
|
|
180
|
+
const websocketBody = withResponsesLiteWebSocketMetadata(body, requestProfile.responsesMode);
|
|
181
|
+
let websocketStarted = false;
|
|
182
|
+
let websocketRetries = 0;
|
|
183
|
+
const maxWebSocketRetries = webSocketStreamMaxRetries(options);
|
|
184
|
+
while (true) {
|
|
185
|
+
websocketStarted = false;
|
|
186
|
+
try {
|
|
187
|
+
await processWebSocketStream(
|
|
188
|
+
websocketUrl,
|
|
189
|
+
websocketBody,
|
|
190
|
+
websocketHeaders,
|
|
191
|
+
output,
|
|
192
|
+
stream,
|
|
193
|
+
model,
|
|
194
|
+
() => {
|
|
195
|
+
websocketStarted = true;
|
|
196
|
+
},
|
|
197
|
+
options,
|
|
198
|
+
deps,
|
|
199
|
+
requestCwd,
|
|
200
|
+
requestPrompt,
|
|
201
|
+
webSearchCitationSources,
|
|
202
|
+
historicalCitationSources,
|
|
203
|
+
websocketRequestMetadata,
|
|
204
|
+
settings.modelProfileHash,
|
|
205
|
+
startupPrewarmTask,
|
|
206
|
+
);
|
|
207
|
+
if (options?.signal?.aborted) {
|
|
208
|
+
throw new Error("Request was aborted");
|
|
209
|
+
}
|
|
210
|
+
finalizeUsage(model, output);
|
|
211
|
+
stream.push({ type: "done", reason: output.stopReason as "stop" | "length" | "toolUse", message: output });
|
|
212
|
+
stream.end();
|
|
213
|
+
return;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
const aborted = options?.signal?.aborted;
|
|
216
|
+
const upgradeRejected = isWebSocketUpgradeRejectedError(error);
|
|
217
|
+
if (
|
|
218
|
+
transport === "auto"
|
|
219
|
+
&& !websocketStarted
|
|
220
|
+
&& upgradeRejected
|
|
221
|
+
) {
|
|
222
|
+
if (fallbackKey) websocketHttpFallbackSessions.add(fallbackKey);
|
|
223
|
+
appendAssistantMessageDiagnostic(
|
|
224
|
+
output,
|
|
225
|
+
createAssistantMessageDiagnostic("provider_transport_failure", error, {
|
|
226
|
+
configuredTransport,
|
|
227
|
+
fallbackTransport: "sse",
|
|
228
|
+
eventsEmitted: false,
|
|
229
|
+
phase: "websocket_upgrade_rejected",
|
|
230
|
+
retries: websocketRetries,
|
|
231
|
+
requestBytes: new TextEncoder().encode(bodyJson).byteLength,
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
const retryableTransport = !aborted
|
|
237
|
+
&& (isWebSocketConnectionLimitReachedError(error) || isRetryableWebSocketError(error));
|
|
238
|
+
const retryableBeforeStart = !websocketStarted && retryableTransport;
|
|
239
|
+
if (retryableBeforeStart && websocketRetries < maxWebSocketRetries) {
|
|
240
|
+
websocketRetries++;
|
|
241
|
+
await sleep(webSocketRetryDelayMs(error, websocketRetries, options), options?.signal);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (aborted || (isProviderNonTransportError(error) && !isWebSocketConnectionLimitReachedError(error))) {
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
appendAssistantMessageDiagnostic(
|
|
248
|
+
output,
|
|
249
|
+
createAssistantMessageDiagnostic("provider_transport_failure", error, {
|
|
250
|
+
configuredTransport,
|
|
251
|
+
eventsEmitted: websocketStarted,
|
|
252
|
+
phase: websocketStarted ? "after_message_stream_start" : "before_message_stream_start",
|
|
253
|
+
retries: websocketRetries,
|
|
254
|
+
requestBytes: new TextEncoder().encode(bodyJson).byteLength,
|
|
255
|
+
}),
|
|
256
|
+
);
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let response: Response | undefined;
|
|
263
|
+
let lastError: Error | undefined;
|
|
264
|
+
const sseUrl = resolveCodexUrl(model.baseUrl, { apiKeyMode: apiKeyTransport });
|
|
265
|
+
const sseDispatcher = await proxyDispatcherForUrl(sseUrl);
|
|
266
|
+
if (transformHeaders) {
|
|
267
|
+
sseHeaders = providerHeadersToHeaders(
|
|
268
|
+
await transformHeaders(headersToRecord(sseHeaders)),
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
273
|
+
if (options?.signal?.aborted) {
|
|
274
|
+
throw new Error("Request was aborted");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
response = await fetchWithResponseHeaderTimeout(sseUrl, {
|
|
279
|
+
method: "POST",
|
|
280
|
+
headers: sseHeaders,
|
|
281
|
+
body: bodyJson,
|
|
282
|
+
...(sseDispatcher ? { dispatcher: sseDispatcher } : {}),
|
|
283
|
+
} as RequestInit, options?.signal, responseHeaderTimeoutMs);
|
|
284
|
+
|
|
285
|
+
await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
|
|
286
|
+
|
|
287
|
+
if (response.ok) {
|
|
288
|
+
if (options?.sessionId) {
|
|
289
|
+
captureCodexTurnState(
|
|
290
|
+
options.sessionId,
|
|
291
|
+
response.headers.get("x-codex-turn-state") ?? undefined,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const errorText = await response.text();
|
|
298
|
+
if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
|
|
299
|
+
await sleep(BASE_DELAY_MS * 2 ** attempt, options?.signal);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const fakeResponse = new Response(errorText, {
|
|
304
|
+
status: response.status,
|
|
305
|
+
statusText: response.statusText,
|
|
306
|
+
});
|
|
307
|
+
const info = await parseErrorResponse(fakeResponse);
|
|
308
|
+
throw new NonRetryableProviderError(withHttpStatusPrefix(response.status, info.friendlyMessage || info.message));
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error instanceof NonRetryableProviderError) {
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
if (error instanceof Error && (error.name === "AbortError" || error.message === "Request was aborted")) {
|
|
314
|
+
throw new Error("Request was aborted");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
318
|
+
if (attempt < MAX_RETRIES && !lastError.message.includes("usage limit")) {
|
|
319
|
+
await sleep(BASE_DELAY_MS * 2 ** attempt, options?.signal);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
throw lastError;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!response?.ok) {
|
|
327
|
+
throw lastError ?? new Error("Failed after retries");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!response.body) {
|
|
331
|
+
throw new Error("No response body");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
stream.push({ type: "start", partial: output });
|
|
335
|
+
await processCapturedResponsesStream(
|
|
336
|
+
parseSSE(response),
|
|
337
|
+
output,
|
|
338
|
+
stream,
|
|
339
|
+
model,
|
|
340
|
+
options,
|
|
341
|
+
options?.sessionId,
|
|
342
|
+
deps,
|
|
343
|
+
requestCwd,
|
|
344
|
+
requestPrompt,
|
|
345
|
+
webSearchCitationSources,
|
|
346
|
+
historicalCitationSources,
|
|
347
|
+
);
|
|
348
|
+
finalizeUsage(model, output);
|
|
349
|
+
|
|
350
|
+
if (options?.signal?.aborted) {
|
|
351
|
+
throw new Error("Request was aborted");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
stream.push({ type: "done", reason: output.stopReason as "stop" | "length" | "toolUse", message: output });
|
|
355
|
+
stream.end();
|
|
356
|
+
} catch (error) {
|
|
357
|
+
stream.push({
|
|
358
|
+
type: "error",
|
|
359
|
+
reason: (options?.signal?.aborted ? "aborted" : "error") as "aborted" | "error",
|
|
360
|
+
error: createErrorMessage(output, error, !!options?.signal?.aborted),
|
|
361
|
+
});
|
|
362
|
+
stream.end();
|
|
363
|
+
}
|
|
364
|
+
})();
|
|
365
|
+
|
|
366
|
+
return stream;
|
|
367
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { DEFAULT_CODEX_BASE_URL } from "./constants.js";
|
|
2
|
+
|
|
3
|
+
export function resolveCodexUrl(baseUrl: string | undefined, options?: { apiKeyMode?: boolean }): string {
|
|
4
|
+
const raw = baseUrl && baseUrl.trim().length > 0 ? baseUrl : DEFAULT_CODEX_BASE_URL;
|
|
5
|
+
const normalized = raw.replace(/\/+$/, "");
|
|
6
|
+
if (options?.apiKeyMode) {
|
|
7
|
+
if (normalized.endsWith("/responses")) return normalized;
|
|
8
|
+
return `${normalized}/responses`;
|
|
9
|
+
}
|
|
10
|
+
if (normalized.endsWith("/codex/responses")) return normalized;
|
|
11
|
+
if (normalized.endsWith("/codex")) return `${normalized}/responses`;
|
|
12
|
+
return `${normalized}/codex/responses`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resolveResponsesWebSocketUrl(baseUrl: string | undefined, options?: { apiKeyMode?: boolean }): string {
|
|
16
|
+
const url = new URL(resolveCodexUrl(baseUrl, options));
|
|
17
|
+
if (url.protocol === "https:") url.protocol = "wss:";
|
|
18
|
+
if (url.protocol === "http:") url.protocol = "ws:";
|
|
19
|
+
return url.toString();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function compactUrl(baseUrl: string | undefined, apiKeyMode: boolean): string {
|
|
23
|
+
return `${resolveCodexUrl(baseUrl, { apiKeyMode }).replace(/\/+$/, "")}/compact`;
|
|
24
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type Api, type AssistantMessage, type Model, type SimpleStreamOptions } from "@earendil-works/pi-ai/compat";
|
|
2
|
+
import { loadModelSettings } from "@oai404iao/pi-codex-runtime/internal/model-catalog/runtime";
|
|
3
|
+
import { type ServiceTier } from "@oai404iao/pi-codex-runtime/internal/providers/openai-codex/types";
|
|
4
|
+
|
|
5
|
+
function getServiceTierCostMultiplier(
|
|
6
|
+
model: Model<Api>,
|
|
7
|
+
serviceTier: ServiceTier,
|
|
8
|
+
cwd: string,
|
|
9
|
+
): number {
|
|
10
|
+
if (serviceTier === "flex") return 0.5;
|
|
11
|
+
const settings = loadModelSettings(model, cwd);
|
|
12
|
+
return serviceTier && serviceTier === settings.fastServiceTier
|
|
13
|
+
? settings.fastCostMultiplier ?? 1
|
|
14
|
+
: 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function applyServiceTierPricing(
|
|
18
|
+
usage: AssistantMessage["usage"],
|
|
19
|
+
serviceTier: ServiceTier,
|
|
20
|
+
model: Model<Api>,
|
|
21
|
+
cwd: string,
|
|
22
|
+
): void {
|
|
23
|
+
const multiplier = getServiceTierCostMultiplier(model, serviceTier, cwd);
|
|
24
|
+
if (multiplier === 1) return;
|
|
25
|
+
usage.cost.input *= multiplier;
|
|
26
|
+
usage.cost.output *= multiplier;
|
|
27
|
+
usage.cost.cacheRead *= multiplier;
|
|
28
|
+
usage.cost.cacheWrite *= multiplier;
|
|
29
|
+
usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveCodexServiceTier(responseServiceTier: ServiceTier, requestServiceTier: ServiceTier): ServiceTier {
|
|
33
|
+
if (
|
|
34
|
+
responseServiceTier === "default"
|
|
35
|
+
&& (requestServiceTier === "flex" || requestServiceTier === "priority")
|
|
36
|
+
) {
|
|
37
|
+
return requestServiceTier;
|
|
38
|
+
}
|
|
39
|
+
return responseServiceTier ?? requestServiceTier;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function withRequestServiceTier(
|
|
43
|
+
options: SimpleStreamOptions | undefined,
|
|
44
|
+
serviceTier: unknown,
|
|
45
|
+
): SimpleStreamOptions | undefined {
|
|
46
|
+
if (
|
|
47
|
+
serviceTier !== "auto"
|
|
48
|
+
&& serviceTier !== "default"
|
|
49
|
+
&& serviceTier !== "flex"
|
|
50
|
+
&& serviceTier !== "scale"
|
|
51
|
+
&& serviceTier !== "priority"
|
|
52
|
+
) {
|
|
53
|
+
return options;
|
|
54
|
+
}
|
|
55
|
+
return { ...options, serviceTier } as SimpleStreamOptions;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function finalizeUsage<TApi extends Api>(model: Model<TApi>, output: AssistantMessage): void {
|
|
59
|
+
output.usage.cost.total = output.usage.cost.input + output.usage.cost.output + output.usage.cost.cacheRead + output.usage.cost.cacheWrite;
|
|
60
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { WEBSOCKET_CONNECT_TIMEOUT_MS } from "./constants.js";
|
|
2
|
+
import { WebSocketHandshakeError, extractWebSocketCloseError, extractWebSocketError } from "./errors.js";
|
|
3
|
+
import { proxyForWebSocketUrl } from "./proxy.js";
|
|
4
|
+
import { dynamicImport } from "./runtime.js";
|
|
5
|
+
import { type NodeWebSocketModule, type WebSocketLike } from "@oai404iao/pi-codex-runtime/internal/providers/openai-codex/types";
|
|
6
|
+
|
|
7
|
+
let nodeWebSocketModulePromise: Promise<NodeWebSocketModule> | undefined;
|
|
8
|
+
|
|
9
|
+
async function loadNodeWebSocketModule(): Promise<NodeWebSocketModule> {
|
|
10
|
+
if (!nodeWebSocketModulePromise) {
|
|
11
|
+
nodeWebSocketModulePromise = dynamicImport("ws") as Promise<NodeWebSocketModule>;
|
|
12
|
+
}
|
|
13
|
+
return nodeWebSocketModulePromise;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function nodeWebSocketHeaders(headers: Headers): Record<string, string> {
|
|
17
|
+
return Object.fromEntries(headers.entries());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function nodeWebSocketResponseHeaders(headers: unknown): Record<string, string> {
|
|
21
|
+
if (!headers || typeof headers !== "object") return {};
|
|
22
|
+
const result: Record<string, string> = {};
|
|
23
|
+
for (const [name, value] of Object.entries(headers as Record<string, unknown>)) {
|
|
24
|
+
if (Array.isArray(value)) result[name] = value.join(", ");
|
|
25
|
+
else if (typeof value === "string") result[name] = value;
|
|
26
|
+
else if (value !== undefined) result[name] = String(value);
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function handshakeMessage(status: number, statusText: string | undefined, body: string): string {
|
|
32
|
+
const trimmedBody = body.trim();
|
|
33
|
+
if (trimmedBody) {
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(trimmedBody) as { error?: { message?: unknown }; message?: unknown };
|
|
36
|
+
const message = typeof parsed.error?.message === "string"
|
|
37
|
+
? parsed.error.message
|
|
38
|
+
: typeof parsed.message === "string"
|
|
39
|
+
? parsed.message
|
|
40
|
+
: undefined;
|
|
41
|
+
if (message?.trim()) return message.trim();
|
|
42
|
+
} catch {
|
|
43
|
+
return trimmedBody;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return statusText?.trim() || "WebSocket upgrade failed";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function connectWebSocket(
|
|
50
|
+
url: string,
|
|
51
|
+
headers: Headers,
|
|
52
|
+
signal: AbortSignal | undefined,
|
|
53
|
+
timeoutMs = WEBSOCKET_CONNECT_TIMEOUT_MS,
|
|
54
|
+
): Promise<WebSocketLike> {
|
|
55
|
+
if (signal?.aborted) throw new Error("Request was aborted");
|
|
56
|
+
const { WebSocket } = await loadNodeWebSocketModule();
|
|
57
|
+
const proxy = proxyForWebSocketUrl(url);
|
|
58
|
+
let agent: unknown;
|
|
59
|
+
if (proxy) {
|
|
60
|
+
const protocol = new URL(proxy).protocol.toLowerCase();
|
|
61
|
+
if (protocol === "http:" || protocol === "https:") {
|
|
62
|
+
const { HttpsProxyAgent } = await dynamicImport("https-proxy-agent") as typeof import("https-proxy-agent");
|
|
63
|
+
agent = new HttpsProxyAgent(proxy);
|
|
64
|
+
} else if (protocol === "socks:" || protocol === "socks4:" || protocol === "socks4a:" || protocol === "socks5:" || protocol === "socks5h:") {
|
|
65
|
+
const { SocksProxyAgent } = await dynamicImport("socks-proxy-agent") as {
|
|
66
|
+
SocksProxyAgent: new (proxy: string) => unknown;
|
|
67
|
+
};
|
|
68
|
+
agent = new SocksProxyAgent(proxy);
|
|
69
|
+
} else {
|
|
70
|
+
throw new Error(`Unsupported WebSocket proxy protocol: ${protocol}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
let settled = false;
|
|
76
|
+
let socket: InstanceType<NodeWebSocketModule["WebSocket"]>;
|
|
77
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
socket = new WebSocket(url, {
|
|
81
|
+
headers: nodeWebSocketHeaders(headers),
|
|
82
|
+
perMessageDeflate: true,
|
|
83
|
+
...(agent ? { agent } : {}),
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const onOpen = () => {
|
|
91
|
+
if (settled) return;
|
|
92
|
+
settled = true;
|
|
93
|
+
cleanup();
|
|
94
|
+
// Keep an error listener installed while the socket sits idle in the
|
|
95
|
+
// session cache. Request parsers add their own listener, but Node's ws
|
|
96
|
+
// EventEmitter would otherwise treat an idle "error" as uncaught.
|
|
97
|
+
socket.on("error", () => {});
|
|
98
|
+
const messageListeners = new Map<(event: unknown) => void, (...args: any[]) => void>();
|
|
99
|
+
const closeListeners = new Map<(event: unknown) => void, (...args: any[]) => void>();
|
|
100
|
+
resolve({
|
|
101
|
+
get readyState() {
|
|
102
|
+
return socket.readyState;
|
|
103
|
+
},
|
|
104
|
+
get bufferedAmount() {
|
|
105
|
+
return socket.bufferedAmount;
|
|
106
|
+
},
|
|
107
|
+
send(data, callback) {
|
|
108
|
+
socket.send(data, callback);
|
|
109
|
+
},
|
|
110
|
+
close(code, reason) {
|
|
111
|
+
socket.close(code, reason);
|
|
112
|
+
},
|
|
113
|
+
addEventListener(type, listener) {
|
|
114
|
+
if (type === "message") {
|
|
115
|
+
const wrapped = (data: unknown, isBinary: boolean) => listener({ data, isBinary });
|
|
116
|
+
messageListeners.set(listener, wrapped);
|
|
117
|
+
socket.on("message", wrapped);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (type === "close") {
|
|
121
|
+
const wrapped = (code: number, reason: Buffer) => listener({
|
|
122
|
+
code,
|
|
123
|
+
reason: reason.toString("utf8"),
|
|
124
|
+
});
|
|
125
|
+
closeListeners.set(listener, wrapped);
|
|
126
|
+
socket.on("close", wrapped);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
socket.on(type, listener as (...args: any[]) => void);
|
|
130
|
+
},
|
|
131
|
+
removeEventListener(type, listener) {
|
|
132
|
+
if (type === "message") {
|
|
133
|
+
const wrapped = messageListeners.get(listener);
|
|
134
|
+
if (wrapped) socket.off("message", wrapped);
|
|
135
|
+
messageListeners.delete(listener);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (type === "close") {
|
|
139
|
+
const wrapped = closeListeners.get(listener);
|
|
140
|
+
if (wrapped) socket.off("close", wrapped);
|
|
141
|
+
closeListeners.delete(listener);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
socket.off(type, listener as (...args: any[]) => void);
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
const onError = (event: unknown) => {
|
|
149
|
+
if (settled) return;
|
|
150
|
+
settled = true;
|
|
151
|
+
cleanup();
|
|
152
|
+
reject(event instanceof Error ? event : extractWebSocketError(event));
|
|
153
|
+
};
|
|
154
|
+
const onClose = (code: number, reason: Buffer) => {
|
|
155
|
+
if (settled) return;
|
|
156
|
+
settled = true;
|
|
157
|
+
cleanup();
|
|
158
|
+
reject(extractWebSocketCloseError({ code, reason: reason.toString("utf8") }));
|
|
159
|
+
};
|
|
160
|
+
const onUnexpectedResponse = (
|
|
161
|
+
_request: unknown,
|
|
162
|
+
response: { statusCode?: number; statusMessage?: string; headers?: unknown; on(type: string, listener: (...args: any[]) => void): void },
|
|
163
|
+
) => {
|
|
164
|
+
if (settled) return;
|
|
165
|
+
let body = "";
|
|
166
|
+
response.on("data", (chunk: unknown) => {
|
|
167
|
+
if (body.length >= 64 * 1024) return;
|
|
168
|
+
body += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
|
169
|
+
});
|
|
170
|
+
response.on("end", () => {
|
|
171
|
+
if (settled) return;
|
|
172
|
+
settled = true;
|
|
173
|
+
cleanup();
|
|
174
|
+
const status = response.statusCode ?? 500;
|
|
175
|
+
reject(new WebSocketHandshakeError(
|
|
176
|
+
status,
|
|
177
|
+
handshakeMessage(status, response.statusMessage, body),
|
|
178
|
+
nodeWebSocketResponseHeaders(response.headers),
|
|
179
|
+
body || undefined,
|
|
180
|
+
));
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
const onAbort = () => {
|
|
184
|
+
if (settled) return;
|
|
185
|
+
settled = true;
|
|
186
|
+
cleanup();
|
|
187
|
+
socket.on("error", () => {});
|
|
188
|
+
socket.terminate?.();
|
|
189
|
+
reject(new Error("Request was aborted"));
|
|
190
|
+
};
|
|
191
|
+
const onTimeout = () => {
|
|
192
|
+
if (settled) return;
|
|
193
|
+
settled = true;
|
|
194
|
+
cleanup();
|
|
195
|
+
socket.on("error", () => {});
|
|
196
|
+
socket.terminate?.();
|
|
197
|
+
reject(new Error(`OpenAI Responses WebSocket connection timed out after ${timeoutMs}ms`));
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const cleanup = () => {
|
|
201
|
+
if (timeout) clearTimeout(timeout);
|
|
202
|
+
socket.off("open", onOpen);
|
|
203
|
+
socket.off("error", onError);
|
|
204
|
+
socket.off("close", onClose);
|
|
205
|
+
socket.off("unexpected-response", onUnexpectedResponse);
|
|
206
|
+
signal?.removeEventListener("abort", onAbort);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
socket.on("open", onOpen);
|
|
210
|
+
socket.on("error", onError);
|
|
211
|
+
socket.on("close", onClose);
|
|
212
|
+
socket.on("unexpected-response", onUnexpectedResponse);
|
|
213
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
214
|
+
timeout = setTimeout(onTimeout, Math.max(1, timeoutMs));
|
|
215
|
+
});
|
|
216
|
+
}
|