@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,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
+ }