@oai404iao/pi-codex-minimal-tools 1.4.0 → 2.0.0
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/README.md +44 -17
- package/THIRD_PARTY_NOTICES.md +20 -0
- package/config.schema.json +1 -2
- package/index.ts +2 -0
- package/package.json +14 -13
- package/provenance/openai-codex-ddea03ad-astra.json +54 -0
- package/src/activation.ts +1 -56
- package/src/adapter/compaction/checkpoint.ts +1 -0
- package/src/adapter/compaction/collect.ts +1 -0
- package/src/adapter/compaction/http.ts +1 -0
- package/src/adapter/compaction/request.ts +1 -0
- package/src/adapter/compaction/transport.ts +1 -0
- package/src/adapter/compaction/websocket.ts +1 -0
- package/src/background-image-generation.ts +1 -574
- package/src/capabilities.ts +1 -146
- package/src/codex-http.ts +1 -133
- package/src/codex-identity-extension.ts +1 -339
- package/src/codex-request-profile.ts +1 -45
- package/src/codex-reserved-tools.ts +1 -323
- package/src/codex-wire-identity.ts +1 -596
- package/src/extension/prewarm-snapshot.ts +1 -0
- package/src/extension/provider-presentation.ts +1 -0
- package/src/extension/provider-runtime.ts +1 -0
- package/src/extension/register.ts +36 -0
- package/src/extension/startup-prewarm.ts +1 -0
- package/src/fast-mode.ts +1 -124
- package/src/glyphs.ts +1 -70
- package/src/index.ts +8 -331
- package/src/model-catalog/catalog.ts +1 -636
- package/src/model-catalog/default-models.json +8 -0
- package/src/model-catalog/runtime.ts +1 -113
- package/src/model-catalog/types.ts +1 -95
- package/src/native-compaction.ts +1 -393
- package/src/patch/apply.ts +1 -338
- package/src/patch/parser.ts +1 -224
- package/src/patch/render.ts +1 -201
- package/src/provider-headers.ts +1 -54
- package/src/provider-native-tools.ts +1 -71
- package/src/provider-shim.ts +20 -4511
- package/src/providers/codex-apply-patch-tool.ts +1 -23
- package/src/providers/openai-codex/cache-key.ts +1 -0
- package/src/providers/openai-codex/captured-stream.ts +1 -0
- package/src/providers/openai-codex/constants.ts +1 -0
- package/src/providers/openai-codex/continuation.ts +1 -0
- package/src/providers/openai-codex/errors.ts +1 -0
- package/src/providers/openai-codex/events.ts +1 -0
- package/src/providers/openai-codex/headers.ts +1 -0
- package/src/providers/openai-codex/lite.ts +1 -0
- package/src/providers/openai-codex/message.ts +1 -0
- package/src/providers/openai-codex/prewarm.ts +1 -0
- package/src/providers/openai-codex/proxy.ts +1 -0
- package/src/providers/openai-codex/reasoning.ts +1 -0
- package/src/providers/openai-codex/request-body.ts +1 -0
- package/src/providers/openai-codex/request-context.ts +1 -0
- package/src/providers/openai-codex/request-metadata.ts +1 -0
- package/src/providers/openai-codex/retry.ts +1 -0
- package/src/providers/openai-codex/runtime.ts +1 -0
- package/src/providers/openai-codex/sse.ts +1 -0
- package/src/providers/openai-codex/stream-effects.ts +1 -0
- package/src/providers/openai-codex/stream.ts +1 -0
- package/src/providers/openai-codex/types.ts +1 -0
- package/src/providers/openai-codex/urls.ts +1 -0
- package/src/providers/openai-codex/usage.ts +1 -0
- package/src/providers/openai-codex/websocket-connection.ts +1 -0
- package/src/providers/openai-codex/websocket-events.ts +1 -0
- package/src/providers/openai-codex/websocket-session.ts +1 -0
- package/src/providers/openai-codex/websocket-socket.ts +1 -0
- package/src/providers/openai-codex/websocket-stream.ts +1 -0
- package/src/providers/openai-responses-shared.ts +7 -1463
- package/src/providers/responses/citations.ts +1 -0
- package/src/providers/responses/history.ts +1 -0
- package/src/providers/responses/items.ts +1 -0
- package/src/providers/responses/markdown.ts +1 -0
- package/src/providers/responses/messages.ts +1 -0
- package/src/providers/responses/signatures.ts +1 -0
- package/src/providers/responses/stream-state.ts +1 -0
- package/src/providers/responses/stream.ts +1 -0
- package/src/providers/responses/text-renderer.ts +1 -0
- package/src/providers/responses/text.ts +1 -0
- package/src/providers/responses/tool-identity.ts +1 -0
- package/src/providers/responses/tools.ts +1 -0
- package/src/providers/responses/types.ts +1 -0
- package/src/providers/responses/usage.ts +1 -0
- package/src/settings.ts +1 -247
- package/src/subagent-inline.ts +1 -8
- package/src/tools/apply-patch.ts +1 -84
- package/src/tools/image-generation/capture.ts +1 -0
- package/src/tools/image-generation/display.ts +1 -0
- package/src/tools/image-generation/preview.ts +1 -0
- package/src/tools/image-generation/storage.ts +1 -0
- package/src/tools/image-generation/types.ts +1 -0
- package/src/tools/image-generation.ts +1 -274
- package/src/tools/view-image.ts +1 -98
- package/src/tools/web-search/activity.ts +1 -0
- package/src/tools/web-search/capture.ts +1 -0
- package/src/tools/web-search/render.ts +1 -0
- package/src/tools/web-search.ts +1 -554
- package/src/utils/images.ts +1 -73
- package/src/utils/theme.ts +1 -0
package/src/provider-shim.ts
CHANGED
|
@@ -1,4511 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
type Model,
|
|
22
|
-
type SimpleStreamOptions,
|
|
23
|
-
type ThinkingLevel,
|
|
24
|
-
} from "@earendil-works/pi-ai/compat";
|
|
25
|
-
import type { ResponseCreateParamsStreaming } from "openai/resources/responses/responses.js";
|
|
26
|
-
import { ProxyAgent } from "undici";
|
|
27
|
-
import type { Dispatcher } from "undici";
|
|
28
|
-
import {
|
|
29
|
-
collectHistoricalCitationSources,
|
|
30
|
-
collectWebSearchCitationSources,
|
|
31
|
-
convertResponsesMessages,
|
|
32
|
-
convertResponsesTools,
|
|
33
|
-
encodeWebSearchActivityTextSignature,
|
|
34
|
-
processResponsesStream,
|
|
35
|
-
type CitationSource,
|
|
36
|
-
type WebSearchCitationSource,
|
|
37
|
-
} from "./providers/openai-responses-shared.js";
|
|
38
|
-
import { createCodexApplyPatchCustomTool } from "./providers/codex-apply-patch-tool.js";
|
|
39
|
-
import { createCodexReservedNamespaceTool } from "./codex-reserved-tools.js";
|
|
40
|
-
import { rewriteNativeOpenAiTools } from "./provider-native-tools.js";
|
|
41
|
-
import {
|
|
42
|
-
captureCodexTurnState,
|
|
43
|
-
codexTurnStateFor,
|
|
44
|
-
currentCodexTurn,
|
|
45
|
-
isUuidV7,
|
|
46
|
-
resolveCodexRequestIdentity,
|
|
47
|
-
resolveCodexWireIdentity,
|
|
48
|
-
type CodexRequestIdentity,
|
|
49
|
-
uuidV7,
|
|
50
|
-
} from "./codex-wire-identity.js";
|
|
51
|
-
import { installCodexIdentityLifecycle } from "./codex-identity-extension.js";
|
|
52
|
-
import { applyFastModeServiceTier } from "./fast-mode.js";
|
|
53
|
-
import {
|
|
54
|
-
hasCodexRequestAuth,
|
|
55
|
-
resolveCodexRequestAccountId,
|
|
56
|
-
} from "./codex-http.js";
|
|
57
|
-
import {
|
|
58
|
-
isProviderHeaderSuppressed,
|
|
59
|
-
mergeProviderHeaders,
|
|
60
|
-
providerHeaderDirective,
|
|
61
|
-
setProviderDefaultHeader,
|
|
62
|
-
setProviderGeneratedHeader,
|
|
63
|
-
} from "./provider-headers.js";
|
|
64
|
-
|
|
65
|
-
const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
|
66
|
-
export const IMAGE_SAVE_DISPLAY_MESSAGE_TYPE = "codex-image-generation-display";
|
|
67
|
-
export const WEB_SEARCH_ACTIVITY_MESSAGE_TYPE = "codex-web-search-activity";
|
|
68
|
-
const OPENAI_CODEX_IMAGE_DIR = ".pi/openai-codex-images";
|
|
69
|
-
const OPENAI_CODEX_LATEST_IMAGE_NAME = "latest.png";
|
|
70
|
-
const MAX_RETRIES = 3;
|
|
71
|
-
const BASE_DELAY_MS = 1000;
|
|
72
|
-
const SSE_RESPONSE_HEADER_TIMEOUT_MS = 20_000;
|
|
73
|
-
const WEBSOCKET_CONNECT_TIMEOUT_MS = 15_000;
|
|
74
|
-
const WEBSOCKET_PREWARM_TIMEOUT_MS = 15_000;
|
|
75
|
-
const WEBSOCKET_IDLE_TIMEOUT_MS = 300_000;
|
|
76
|
-
const WEBSOCKET_SEND_TIMEOUT_MS = 300_000;
|
|
77
|
-
const WEBSOCKET_EVENT_QUEUE_CAPACITY = 1600;
|
|
78
|
-
const DEFAULT_WEBSOCKET_STREAM_MAX_RETRIES = 5;
|
|
79
|
-
const MAX_WEBSOCKET_STREAM_MAX_RETRIES = 100;
|
|
80
|
-
const WEBSOCKET_RETRY_BASE_DELAY_MS = 200;
|
|
81
|
-
const WEBSOCKET_RETRY_MAX_DELAY_MS = 60_000;
|
|
82
|
-
const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
|
|
83
|
-
const WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE = "websocket_connection_limit_reached";
|
|
84
|
-
const PREVIOUS_RESPONSE_NOT_FOUND_CODE = "previous_response_not_found";
|
|
85
|
-
const CODEX_RESPONSE_STATUSES = new Set(["completed", "incomplete", "failed", "cancelled", "queued", "in_progress"]);
|
|
86
|
-
const OPENAI_BETA_RESPONSES_WEBSOCKETS = "responses_websockets=2026-02-06";
|
|
87
|
-
const X_OPENAI_INTERNAL_CODEX_RESPONSES_LITE = "x-openai-internal-codex-responses-lite";
|
|
88
|
-
const WS_RESPONSES_LITE_CLIENT_METADATA_KEY = "ws_request_header_x_openai_internal_codex_responses_lite";
|
|
89
|
-
const WS_STREAM_REQUEST_START_MS_CLIENT_METADATA_KEY = "x-codex-ws-stream-request-start-ms";
|
|
90
|
-
const WEB_SEARCH_SOURCES_INCLUDE = "web_search_call.action.sources";
|
|
91
|
-
const WEB_SEARCH_RESULTS_INCLUDE = "web_search_call.results";
|
|
92
|
-
const SESSION_WEBSOCKET_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
93
|
-
const CODEX_COMPACTION_TRIGGER_TYPE = "compaction_trigger";
|
|
94
|
-
const CODEX_RETAINED_MESSAGE_TOKEN_BUDGET = 64_000;
|
|
95
|
-
const CODEX_MAX_RETAINED_AGENT_MESSAGE_TOKENS = 10_000;
|
|
96
|
-
const CODEX_REMOTE_COMPACTION_STREAM_RETRIES = 2;
|
|
97
|
-
const X_CODEX_BETA_FEATURES = "x-codex-beta-features";
|
|
98
|
-
const CODEX_REMOTE_COMPACTION_V2_FEATURE = "remote_compaction_v2";
|
|
99
|
-
const APPROX_BYTES_PER_TOKEN = 4;
|
|
100
|
-
const dynamicImport = (specifier: string) => import(specifier);
|
|
101
|
-
let _os: { platform(): string; release(): string; arch(): string } | null = null;
|
|
102
|
-
|
|
103
|
-
if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
|
|
104
|
-
dynamicImport("node:os")
|
|
105
|
-
.then((module) => {
|
|
106
|
-
_os = module;
|
|
107
|
-
})
|
|
108
|
-
.catch(() => {
|
|
109
|
-
_os = null;
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export interface SavedGeneratedImage {
|
|
114
|
-
absolutePath: string;
|
|
115
|
-
relativePath: string;
|
|
116
|
-
latestAbsolutePath: string;
|
|
117
|
-
latestRelativePath: string;
|
|
118
|
-
responseId: string | undefined;
|
|
119
|
-
callId: string;
|
|
120
|
-
outputFormat: string;
|
|
121
|
-
imageModel?: string;
|
|
122
|
-
revisedPrompt?: string;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
interface ImageDisplayMessageDetails {
|
|
126
|
-
savedImages: SavedGeneratedImage[];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
interface PendingImageDisplay {
|
|
130
|
-
savedImage: SavedGeneratedImage;
|
|
131
|
-
imageData: { data: string; mimeType: string };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
interface QueuedImageActivity extends PendingImageDisplay {
|
|
135
|
-
kind: "image";
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export interface SurfacedWebSearch {
|
|
139
|
-
callId: string;
|
|
140
|
-
status?: string;
|
|
141
|
-
completed?: boolean;
|
|
142
|
-
actionType?: string;
|
|
143
|
-
query?: string;
|
|
144
|
-
queries: string[];
|
|
145
|
-
url?: string;
|
|
146
|
-
pattern?: string;
|
|
147
|
-
sources: Array<{ title?: string; url: string }>;
|
|
148
|
-
responseItem?: Record<string, unknown>;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
type PendingActivity = QueuedImageActivity;
|
|
152
|
-
|
|
153
|
-
interface CachedImagePreview {
|
|
154
|
-
data: string;
|
|
155
|
-
mimeType: string;
|
|
156
|
-
bytes: number;
|
|
157
|
-
widthPx?: number;
|
|
158
|
-
heightPx?: number;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
interface WebSocketLike {
|
|
162
|
-
readyState?: number;
|
|
163
|
-
bufferedAmount?: number;
|
|
164
|
-
send(data: string, callback?: (error?: Error) => void): void;
|
|
165
|
-
close(code?: number, reason?: string): void;
|
|
166
|
-
addEventListener(type: string, listener: (event: unknown) => void): void;
|
|
167
|
-
removeEventListener(type: string, listener: (event: unknown) => void): void;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
interface SessionWebSocketCacheEntry {
|
|
171
|
-
socket: WebSocketLike;
|
|
172
|
-
busy: boolean;
|
|
173
|
-
waiters: WebSocketAcquireWaiter[];
|
|
174
|
-
idleTimer?: ReturnType<typeof setTimeout>;
|
|
175
|
-
continuation?: CachedWebSocketContinuationState;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
interface WebSocketAcquireWaiter {
|
|
179
|
-
resolve: (acquired: AcquiredWebSocket) => void;
|
|
180
|
-
reject: (error: Error) => void;
|
|
181
|
-
signal?: AbortSignal;
|
|
182
|
-
onAbort?: () => void;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
interface AcquiredWebSocket {
|
|
186
|
-
socket: WebSocketLike;
|
|
187
|
-
entry?: SessionWebSocketCacheEntry;
|
|
188
|
-
reused: boolean;
|
|
189
|
-
release: (options?: { keep?: boolean }) => void;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
interface CachedWebSocketContinuationState {
|
|
193
|
-
lastRequestBody: ResponsesBody;
|
|
194
|
-
lastResponseId: string;
|
|
195
|
-
lastResponseItems: unknown[];
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
interface WebSocketPrewarmRequest {
|
|
199
|
-
url: string;
|
|
200
|
-
headers: Headers;
|
|
201
|
-
cacheKey: string;
|
|
202
|
-
body: ResponsesBody;
|
|
203
|
-
requestMetadata: WebSocketRequestMetadata;
|
|
204
|
-
signal?: AbortSignal;
|
|
205
|
-
connectTimeoutMs?: number;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
type StartupPrewarmStatus = "pending" | "ready" | "failed";
|
|
209
|
-
|
|
210
|
-
interface StartupPrewarmState {
|
|
211
|
-
status: StartupPrewarmStatus;
|
|
212
|
-
promise: Promise<void>;
|
|
213
|
-
abortController: AbortController;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
interface SessionStartupPrewarmTask {
|
|
217
|
-
generation: number;
|
|
218
|
-
modelIdentity: string;
|
|
219
|
-
promise: Promise<void>;
|
|
220
|
-
abortController: AbortController;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
interface WebSocketRequestMetadata {
|
|
224
|
-
/** Legacy Pi session lookup key used by exported test helpers. */
|
|
225
|
-
sessionId?: string;
|
|
226
|
-
threadId?: string;
|
|
227
|
-
turnId: string;
|
|
228
|
-
requestKind?: "turn" | "compaction" | "prewarm";
|
|
229
|
-
identity?: CodexRequestIdentity;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export interface OpenAIResponsesProviderController {
|
|
233
|
-
getCurrentTurnId(sessionId: string | undefined): string | undefined;
|
|
234
|
-
getRequestIdentity?(
|
|
235
|
-
sessionId: string | undefined,
|
|
236
|
-
requestKind?: CodexRequestIdentity["requestKind"],
|
|
237
|
-
): CodexRequestIdentity | undefined;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
let fsPromisesPromise: Promise<typeof import("node:fs/promises")> | undefined;
|
|
241
|
-
const workspaceRootCache = new Map<string, Promise<string>>();
|
|
242
|
-
|
|
243
|
-
const PATH_SEPARATOR = "/";
|
|
244
|
-
|
|
245
|
-
interface ResponsesBody {
|
|
246
|
-
model: string;
|
|
247
|
-
store: boolean;
|
|
248
|
-
stream: boolean;
|
|
249
|
-
instructions?: string;
|
|
250
|
-
previous_response_id?: string;
|
|
251
|
-
input: unknown[];
|
|
252
|
-
text: { verbosity: string };
|
|
253
|
-
include: string[];
|
|
254
|
-
prompt_cache_key?: string;
|
|
255
|
-
tool_choice: "auto";
|
|
256
|
-
parallel_tool_calls: boolean;
|
|
257
|
-
temperature?: number;
|
|
258
|
-
service_tier?: string;
|
|
259
|
-
generate?: boolean;
|
|
260
|
-
tools?: unknown[];
|
|
261
|
-
reasoning?: {
|
|
262
|
-
effort?: string;
|
|
263
|
-
summary?: string;
|
|
264
|
-
context?: "all_turns";
|
|
265
|
-
};
|
|
266
|
-
client_metadata?: Record<string, string>;
|
|
267
|
-
[key: string]: unknown;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
interface ResponseEnvelope {
|
|
271
|
-
id?: string;
|
|
272
|
-
status?: string;
|
|
273
|
-
usage?: {
|
|
274
|
-
input_tokens?: number;
|
|
275
|
-
output_tokens?: number;
|
|
276
|
-
total_tokens?: number;
|
|
277
|
-
input_tokens_details?: { cached_tokens?: number };
|
|
278
|
-
};
|
|
279
|
-
service_tier?: string;
|
|
280
|
-
error?: { message?: string };
|
|
281
|
-
[key: string]: unknown;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
type ServiceTier = ResponseCreateParamsStreaming["service_tier"];
|
|
285
|
-
type ProviderTransport = NonNullable<SimpleStreamOptions["transport"]>;
|
|
286
|
-
|
|
287
|
-
const websocketSessionCache = new Map<string, SessionWebSocketCacheEntry>();
|
|
288
|
-
const websocketConnectionPromises = new Map<string, Promise<SessionWebSocketCacheEntry>>();
|
|
289
|
-
const websocketHttpFallbackSessions = new Set<string>();
|
|
290
|
-
|
|
291
|
-
class NonRetryableProviderError extends Error {}
|
|
292
|
-
class ProviderResponseError extends Error {
|
|
293
|
-
code?: string;
|
|
294
|
-
errorType?: string;
|
|
295
|
-
status?: number;
|
|
296
|
-
retryAfterMs?: number;
|
|
297
|
-
}
|
|
298
|
-
class ProviderProtocolError extends Error {}
|
|
299
|
-
class WebSocketHandshakeError extends Error {
|
|
300
|
-
constructor(
|
|
301
|
-
public readonly status: number,
|
|
302
|
-
message: string,
|
|
303
|
-
public readonly headers: Record<string, string> = {},
|
|
304
|
-
public readonly body?: string,
|
|
305
|
-
) {
|
|
306
|
-
super(withHttpStatusPrefix(status, message));
|
|
307
|
-
this.name = "WebSocketHandshakeError";
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
interface NodeWebSocketModule {
|
|
312
|
-
WebSocket: new (url: string, options?: Record<string, unknown>) => {
|
|
313
|
-
readyState: number;
|
|
314
|
-
bufferedAmount: number;
|
|
315
|
-
send(data: string, callback?: (error?: Error) => void): void;
|
|
316
|
-
close(code?: number, reason?: string): void;
|
|
317
|
-
terminate?(): void;
|
|
318
|
-
on(type: string, listener: (...args: any[]) => void): void;
|
|
319
|
-
off(type: string, listener: (...args: any[]) => void): void;
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
let nodeWebSocketModulePromise: Promise<NodeWebSocketModule> | undefined;
|
|
324
|
-
|
|
325
|
-
const HTTP_STATUS_MESSAGE_PREFIX = /^HTTP\s+\d{3}(?::|\b)/i;
|
|
326
|
-
|
|
327
|
-
interface StreamEventShape {
|
|
328
|
-
type?: string;
|
|
329
|
-
status?: number;
|
|
330
|
-
status_code?: number;
|
|
331
|
-
sequence_number?: number;
|
|
332
|
-
error?: {
|
|
333
|
-
type?: string;
|
|
334
|
-
message?: string;
|
|
335
|
-
code?: string;
|
|
336
|
-
plan_type?: string;
|
|
337
|
-
resets_at?: number;
|
|
338
|
-
[key: string]: unknown;
|
|
339
|
-
};
|
|
340
|
-
response?: ResponseEnvelope;
|
|
341
|
-
item_id?: string;
|
|
342
|
-
output_index?: number;
|
|
343
|
-
item?: {
|
|
344
|
-
id?: string;
|
|
345
|
-
type?: string;
|
|
346
|
-
result?: string | null;
|
|
347
|
-
output_format?: string;
|
|
348
|
-
revised_prompt?: string;
|
|
349
|
-
status?: string;
|
|
350
|
-
[key: string]: unknown;
|
|
351
|
-
};
|
|
352
|
-
code?: string;
|
|
353
|
-
message?: string;
|
|
354
|
-
[key: string]: unknown;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function sanitizeFilePart(value: string | undefined, fallback: string): string {
|
|
358
|
-
const trimmed = (value ?? "").trim();
|
|
359
|
-
if (!trimmed) return fallback;
|
|
360
|
-
return trimmed.replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function shortenFilePart(value: string | undefined, fallback: string): string {
|
|
364
|
-
const safe = sanitizeFilePart(value, fallback);
|
|
365
|
-
const match = /^([a-zA-Z]+_)(.+)$/.exec(safe);
|
|
366
|
-
const prefix = match?.[1] ?? "";
|
|
367
|
-
const body = match?.[2] ?? safe;
|
|
368
|
-
if (body.length <= 12) return `${prefix}${body}`;
|
|
369
|
-
return `${prefix}${body.slice(0, 8)}-${body.slice(-4)}`;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function normalizeImageOutputFormat(value: string | undefined): string {
|
|
373
|
-
const format = (value ?? "png").toLowerCase();
|
|
374
|
-
return format === "png" || format === "jpg" || format === "jpeg" || format === "webp" ? format : "png";
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function shortHash(str: string): string {
|
|
378
|
-
let h1 = 0xdeadbeef;
|
|
379
|
-
let h2 = 0x41c6ce57;
|
|
380
|
-
for (let i = 0; i < str.length; i++) {
|
|
381
|
-
const ch = str.charCodeAt(i);
|
|
382
|
-
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
383
|
-
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
384
|
-
}
|
|
385
|
-
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
386
|
-
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
387
|
-
return (h2 >>> 0).toString(36) + (h1 >>> 0).toString(36);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function normalizePath(value: string): string {
|
|
391
|
-
if (!value) return ".";
|
|
392
|
-
const normalized = value.replace(/\/+/g, PATH_SEPARATOR);
|
|
393
|
-
if (normalized === PATH_SEPARATOR) return normalized;
|
|
394
|
-
return normalized.replace(/\/+$/g, "") || PATH_SEPARATOR;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function joinPaths(...parts: string[]): string {
|
|
398
|
-
if (parts.length === 0) return ".";
|
|
399
|
-
let result = parts[0] ?? "";
|
|
400
|
-
for (let i = 1; i < parts.length; i++) {
|
|
401
|
-
const part = parts[i];
|
|
402
|
-
if (!part) continue;
|
|
403
|
-
if (!result || result.endsWith(PATH_SEPARATOR)) {
|
|
404
|
-
result += part.replace(/^\/+/, "");
|
|
405
|
-
} else {
|
|
406
|
-
result += `${PATH_SEPARATOR}${part.replace(/^\/+/, "")}`;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
return normalizePath(result);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function dirnamePath(value: string): string {
|
|
413
|
-
const normalized = normalizePath(value);
|
|
414
|
-
if (normalized === PATH_SEPARATOR) return PATH_SEPARATOR;
|
|
415
|
-
const index = normalized.lastIndexOf(PATH_SEPARATOR);
|
|
416
|
-
if (index < 0) return ".";
|
|
417
|
-
if (index === 0) return PATH_SEPARATOR;
|
|
418
|
-
return normalized.slice(0, index);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function splitPathSegments(value: string): string[] {
|
|
422
|
-
const normalized = normalizePath(value);
|
|
423
|
-
if (normalized === PATH_SEPARATOR) return [];
|
|
424
|
-
return normalized.replace(/^\/+/, "").split(PATH_SEPARATOR).filter(Boolean);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function relativePath(from: string, to: string): string {
|
|
428
|
-
const normalizedFrom = normalizePath(from);
|
|
429
|
-
const normalizedTo = normalizePath(to);
|
|
430
|
-
if (normalizedFrom === normalizedTo) return "";
|
|
431
|
-
const fromSegments = splitPathSegments(normalizedFrom);
|
|
432
|
-
const toSegments = splitPathSegments(normalizedTo);
|
|
433
|
-
let shared = 0;
|
|
434
|
-
while (shared < fromSegments.length && shared < toSegments.length && fromSegments[shared] === toSegments[shared]) {
|
|
435
|
-
shared++;
|
|
436
|
-
}
|
|
437
|
-
const upSegments = new Array(fromSegments.length - shared).fill("..");
|
|
438
|
-
const downSegments = toSegments.slice(shared);
|
|
439
|
-
return [...upSegments, ...downSegments].join(PATH_SEPARATOR);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
async function getNodeFsPromises(): Promise<typeof import("node:fs/promises")> {
|
|
443
|
-
if (!fsPromisesPromise) {
|
|
444
|
-
fsPromisesPromise = dynamicImport("node:fs/promises") as Promise<typeof import("node:fs/promises")>;
|
|
445
|
-
}
|
|
446
|
-
return fsPromisesPromise;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
function getNodeFsSync(): { readFileSync(path: string): Buffer } | null {
|
|
450
|
-
if (typeof process === "undefined" || !(process.versions?.node || process.versions?.bun)) {
|
|
451
|
-
return null;
|
|
452
|
-
}
|
|
453
|
-
const builtinProcess = process as typeof process & { getBuiltinModule?: (specifier: string) => unknown };
|
|
454
|
-
if (typeof builtinProcess.getBuiltinModule !== "function") {
|
|
455
|
-
return null;
|
|
456
|
-
}
|
|
457
|
-
try {
|
|
458
|
-
const module = builtinProcess.getBuiltinModule("node:fs") as { readFileSync?: (path: string) => Buffer } | undefined;
|
|
459
|
-
return typeof module?.readFileSync === "function" ? { readFileSync: module.readFileSync } : null;
|
|
460
|
-
} catch {
|
|
461
|
-
return null;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
async function pathExists(value: string): Promise<boolean> {
|
|
466
|
-
try {
|
|
467
|
-
const fs = await getNodeFsPromises();
|
|
468
|
-
await fs.access(value);
|
|
469
|
-
return true;
|
|
470
|
-
} catch {
|
|
471
|
-
return false;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
async function resolveWorkspaceRoot(cwd: string): Promise<string> {
|
|
476
|
-
const normalizedCwd = normalizePath(cwd);
|
|
477
|
-
const cached = workspaceRootCache.get(normalizedCwd);
|
|
478
|
-
if (cached) return cached;
|
|
479
|
-
|
|
480
|
-
const promise = (async () => {
|
|
481
|
-
let current = normalizedCwd;
|
|
482
|
-
while (true) {
|
|
483
|
-
if (await pathExists(joinPaths(current, ".git"))) {
|
|
484
|
-
return current;
|
|
485
|
-
}
|
|
486
|
-
const parent = dirnamePath(current);
|
|
487
|
-
if (parent === current || parent === ".") {
|
|
488
|
-
return normalizedCwd;
|
|
489
|
-
}
|
|
490
|
-
current = parent;
|
|
491
|
-
}
|
|
492
|
-
})();
|
|
493
|
-
|
|
494
|
-
workspaceRootCache.set(normalizedCwd, promise);
|
|
495
|
-
return promise;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
export function getOpenAICodexImageDirectory(cwd: string): string {
|
|
499
|
-
return joinPaths(cwd, OPENAI_CODEX_IMAGE_DIR);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
export function getOpenAICodexImagePath(cwd: string, responseId: string | undefined, callId: string, outputFormat?: string): string {
|
|
503
|
-
const ext = normalizeImageOutputFormat(outputFormat);
|
|
504
|
-
const safeCallId = shortenFilePart(callId, "image");
|
|
505
|
-
const safeResponseId = shortenFilePart(responseId, "response");
|
|
506
|
-
return joinPaths(getOpenAICodexImageDirectory(cwd), `${safeCallId}-${safeResponseId}.${ext}`);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
export function getOpenAICodexLatestImagePath(cwd: string): string {
|
|
510
|
-
return joinPaths(getOpenAICodexImageDirectory(cwd), OPENAI_CODEX_LATEST_IMAGE_NAME);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
export function buildGeneratedImageDisplayText(savedImage: SavedGeneratedImage, options?: { expanded?: boolean }): string {
|
|
514
|
-
const lines: string[] = [];
|
|
515
|
-
if (options?.expanded && savedImage.revisedPrompt) {
|
|
516
|
-
lines.push(`Prompt: ${savedImage.revisedPrompt}`);
|
|
517
|
-
}
|
|
518
|
-
lines.push(`File: ${savedImage.relativePath}`);
|
|
519
|
-
return lines.join("\n");
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
export async function saveOpenAICodexGeneratedImage(
|
|
523
|
-
cwd: string,
|
|
524
|
-
image: { responseId?: string; callId: string; result: string; outputFormat?: string; imageModel?: string; revisedPrompt?: string },
|
|
525
|
-
): Promise<SavedGeneratedImage> {
|
|
526
|
-
const workspaceRoot = await resolveWorkspaceRoot(cwd);
|
|
527
|
-
const outputFormat = normalizeImageOutputFormat(image.outputFormat);
|
|
528
|
-
const saved = await saveBase64Image({
|
|
529
|
-
base64: image.result,
|
|
530
|
-
callId: image.callId,
|
|
531
|
-
cwd,
|
|
532
|
-
format: outputFormat,
|
|
533
|
-
responseId: image.responseId,
|
|
534
|
-
settings: loadSettings(cwd),
|
|
535
|
-
});
|
|
536
|
-
const absolutePath = saved.path;
|
|
537
|
-
const latestAbsolutePath = saved.latestPath ?? getOpenAICodexLatestImagePath(workspaceRoot);
|
|
538
|
-
|
|
539
|
-
const relativeFilePath = relativePath(workspaceRoot, absolutePath);
|
|
540
|
-
const latestRelativeFilePath = relativePath(workspaceRoot, latestAbsolutePath);
|
|
541
|
-
const relativePathValue = relativeFilePath && !relativeFilePath.startsWith("..") ? relativeFilePath : absolutePath;
|
|
542
|
-
const latestRelativePathValue =
|
|
543
|
-
latestRelativeFilePath && !latestRelativeFilePath.startsWith("..") ? latestRelativeFilePath : latestAbsolutePath;
|
|
544
|
-
|
|
545
|
-
return {
|
|
546
|
-
absolutePath,
|
|
547
|
-
relativePath: relativePathValue,
|
|
548
|
-
latestAbsolutePath,
|
|
549
|
-
latestRelativePath: latestRelativePathValue,
|
|
550
|
-
responseId: image.responseId,
|
|
551
|
-
callId: image.callId,
|
|
552
|
-
outputFormat,
|
|
553
|
-
imageModel: image.imageModel,
|
|
554
|
-
revisedPrompt: image.revisedPrompt,
|
|
555
|
-
};
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
export function resolveCodexUrl(baseUrl: string | undefined, options?: { apiKeyMode?: boolean }): string {
|
|
559
|
-
const raw = baseUrl && baseUrl.trim().length > 0 ? baseUrl : DEFAULT_CODEX_BASE_URL;
|
|
560
|
-
const normalized = raw.replace(/\/+$/, "");
|
|
561
|
-
if (options?.apiKeyMode) {
|
|
562
|
-
if (normalized.endsWith("/responses")) return normalized;
|
|
563
|
-
return `${normalized}/responses`;
|
|
564
|
-
}
|
|
565
|
-
if (normalized.endsWith("/codex/responses")) return normalized;
|
|
566
|
-
if (normalized.endsWith("/codex")) return `${normalized}/responses`;
|
|
567
|
-
return `${normalized}/codex/responses`;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
export function resolveResponsesWebSocketUrl(baseUrl: string | undefined, options?: { apiKeyMode?: boolean }): string {
|
|
571
|
-
const url = new URL(resolveCodexUrl(baseUrl, options));
|
|
572
|
-
if (url.protocol === "https:") url.protocol = "wss:";
|
|
573
|
-
if (url.protocol === "http:") url.protocol = "ws:";
|
|
574
|
-
return url.toString();
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
function headersToRecord(headers: Headers): Record<string, string> {
|
|
578
|
-
return Object.fromEntries(headers.entries());
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function providerHeadersToHeaders(headers: ProviderHeaders): Headers {
|
|
582
|
-
const result = new Headers();
|
|
583
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
584
|
-
if (typeof value === "string") result.set(name, value);
|
|
585
|
-
}
|
|
586
|
-
return result;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
function createCodexRequestId(): string {
|
|
590
|
-
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
591
|
-
return globalThis.crypto.randomUUID();
|
|
592
|
-
}
|
|
593
|
-
return `codex_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function createPiTurnId(): string {
|
|
597
|
-
return uuidV7();
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
function buildBaseCodexHeaders(
|
|
601
|
-
modelHeaders: ProviderHeaders | undefined,
|
|
602
|
-
additionalHeaders: ProviderHeaders | undefined,
|
|
603
|
-
accountId: string | undefined,
|
|
604
|
-
token: string,
|
|
605
|
-
): Headers {
|
|
606
|
-
const headers = mergeProviderHeaders(modelHeaders, additionalHeaders);
|
|
607
|
-
if (providerHeaderDirective(additionalHeaders, "authorization") === undefined && token) {
|
|
608
|
-
setProviderGeneratedHeader(headers, "Authorization", `Bearer ${token}`);
|
|
609
|
-
}
|
|
610
|
-
if (accountId) setProviderDefaultHeader(headers, "chatgpt-account-id", accountId);
|
|
611
|
-
setProviderDefaultHeader(headers, "originator", "pi");
|
|
612
|
-
setProviderDefaultHeader(
|
|
613
|
-
headers,
|
|
614
|
-
"User-Agent",
|
|
615
|
-
_os ? `pi (${_os.platform()} ${_os.release()}; ${_os.arch()})` : "pi (browser)",
|
|
616
|
-
);
|
|
617
|
-
return headers;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* Resolve the wire identity (UUID v7 session/thread/window) for a pi session
|
|
622
|
-
* and thread. Returns `undefined` when no pi session id is available so
|
|
623
|
-
* session-less requests keep their previous behavior.
|
|
624
|
-
*/
|
|
625
|
-
function wireIdentityFor(
|
|
626
|
-
sessionId: string | undefined,
|
|
627
|
-
threadId?: string,
|
|
628
|
-
): { sessionId: string; threadId: string; windowId: string } | undefined {
|
|
629
|
-
if (!sessionId) return undefined;
|
|
630
|
-
return resolveCodexWireIdentity(sessionId, threadId || sessionId);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
/** Inject the Codex-compatible identity headers for a session. */
|
|
634
|
-
function applyWireIdentityHeaders(
|
|
635
|
-
headers: Headers,
|
|
636
|
-
sessionId: string | undefined,
|
|
637
|
-
threadId: string | undefined,
|
|
638
|
-
requestIdentity?: CodexRequestIdentity,
|
|
639
|
-
): void {
|
|
640
|
-
const wire = requestIdentity ?? wireIdentityFor(sessionId, threadId);
|
|
641
|
-
if (!wire) return;
|
|
642
|
-
setProviderGeneratedHeader(headers, "session-id", wire.sessionId);
|
|
643
|
-
setProviderGeneratedHeader(headers, "thread-id", wire.threadId);
|
|
644
|
-
setProviderGeneratedHeader(headers, "x-codex-window-id", wire.windowId);
|
|
645
|
-
setProviderGeneratedHeader(headers, "x-client-request-id", wire.threadId);
|
|
646
|
-
const turnState = requestIdentity?.turnState
|
|
647
|
-
?? (sessionId ? codexTurnStateFor(sessionId) : undefined);
|
|
648
|
-
if (turnState) {
|
|
649
|
-
setProviderGeneratedHeader(headers, "x-codex-turn-state", turnState);
|
|
650
|
-
}
|
|
651
|
-
if (requestIdentity?.parentThreadId) {
|
|
652
|
-
setProviderGeneratedHeader(
|
|
653
|
-
headers,
|
|
654
|
-
"x-codex-parent-thread-id",
|
|
655
|
-
requestIdentity.parentThreadId,
|
|
656
|
-
);
|
|
657
|
-
}
|
|
658
|
-
if (requestIdentity?.subagentKind) {
|
|
659
|
-
setProviderGeneratedHeader(
|
|
660
|
-
headers,
|
|
661
|
-
"x-openai-subagent",
|
|
662
|
-
requestIdentity.subagentKind,
|
|
663
|
-
);
|
|
664
|
-
}
|
|
665
|
-
if (requestIdentity) {
|
|
666
|
-
setProviderGeneratedHeader(
|
|
667
|
-
headers,
|
|
668
|
-
"x-codex-turn-metadata",
|
|
669
|
-
buildCodexTurnMetadataJson(requestIdentity),
|
|
670
|
-
);
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
/** Codex identity headers for an SSE request (exported for tests). */
|
|
675
|
-
export function buildSSEHeaders(
|
|
676
|
-
modelHeaders: ProviderHeaders | undefined,
|
|
677
|
-
additionalHeaders: ProviderHeaders | undefined,
|
|
678
|
-
accountId: string | undefined,
|
|
679
|
-
token: string,
|
|
680
|
-
sessionId: string | undefined,
|
|
681
|
-
profile: CodexRequestProfile,
|
|
682
|
-
threadId?: string,
|
|
683
|
-
requestIdentity?: CodexRequestIdentity,
|
|
684
|
-
): Headers {
|
|
685
|
-
const headers = buildBaseCodexHeaders(modelHeaders, additionalHeaders, accountId, token);
|
|
686
|
-
setProviderDefaultHeader(headers, "OpenAI-Beta", "responses=experimental");
|
|
687
|
-
setProviderDefaultHeader(headers, "accept", "text/event-stream");
|
|
688
|
-
setProviderDefaultHeader(headers, "content-type", "application/json");
|
|
689
|
-
if (profile.responsesMode === "lite") {
|
|
690
|
-
setProviderDefaultHeader(headers, X_OPENAI_INTERNAL_CODEX_RESPONSES_LITE, "true");
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
applyWireIdentityHeaders(headers, sessionId, threadId, requestIdentity);
|
|
694
|
-
|
|
695
|
-
return headers;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
function appendCommaSeparatedHeader(headers: Headers, name: string, value: string): void {
|
|
699
|
-
if (isProviderHeaderSuppressed(headers, name)) return;
|
|
700
|
-
const values = (headers.get(name) ?? "")
|
|
701
|
-
.split(",")
|
|
702
|
-
.map((entry) => entry.trim())
|
|
703
|
-
.filter(Boolean);
|
|
704
|
-
if (!values.includes(value)) headers.set(name, [...values, value].join(","));
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
function applyConfiguredResponsesFeatureHeaders(
|
|
708
|
-
headers: Headers,
|
|
709
|
-
settings: ResolvedCodexModelSettings,
|
|
710
|
-
_model: Model<Api>,
|
|
711
|
-
): Headers {
|
|
712
|
-
if (settings.compactionMode === "responses") {
|
|
713
|
-
appendCommaSeparatedHeader(
|
|
714
|
-
headers,
|
|
715
|
-
X_CODEX_BETA_FEATURES,
|
|
716
|
-
CODEX_REMOTE_COMPACTION_V2_FEATURE,
|
|
717
|
-
);
|
|
718
|
-
}
|
|
719
|
-
return headers;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
export function buildWebSocketHeaders(
|
|
723
|
-
modelHeaders: ProviderHeaders | undefined,
|
|
724
|
-
additionalHeaders: ProviderHeaders | undefined,
|
|
725
|
-
accountId: string | undefined,
|
|
726
|
-
token: string,
|
|
727
|
-
sessionId: string,
|
|
728
|
-
threadId = sessionId,
|
|
729
|
-
requestIdentity?: CodexRequestIdentity,
|
|
730
|
-
): Headers {
|
|
731
|
-
const headers = buildBaseCodexHeaders(modelHeaders, additionalHeaders, accountId, token);
|
|
732
|
-
headers.delete("accept");
|
|
733
|
-
headers.delete("content-type");
|
|
734
|
-
headers.delete("OpenAI-Beta");
|
|
735
|
-
headers.delete("openai-beta");
|
|
736
|
-
setProviderDefaultHeader(headers, "OpenAI-Beta", OPENAI_BETA_RESPONSES_WEBSOCKETS);
|
|
737
|
-
setProviderDefaultHeader(headers, "x-client-request-id", threadId);
|
|
738
|
-
setProviderDefaultHeader(headers, "session-id", sessionId);
|
|
739
|
-
setProviderDefaultHeader(headers, "thread-id", threadId);
|
|
740
|
-
const wire = requestIdentity ?? wireIdentityFor(sessionId, threadId);
|
|
741
|
-
if (wire) {
|
|
742
|
-
setProviderGeneratedHeader(headers, "session-id", wire.sessionId);
|
|
743
|
-
setProviderGeneratedHeader(headers, "thread-id", wire.threadId);
|
|
744
|
-
setProviderGeneratedHeader(headers, "x-codex-window-id", wire.windowId);
|
|
745
|
-
setProviderGeneratedHeader(headers, "x-client-request-id", wire.threadId);
|
|
746
|
-
}
|
|
747
|
-
if (requestIdentity?.parentThreadId) {
|
|
748
|
-
setProviderGeneratedHeader(
|
|
749
|
-
headers,
|
|
750
|
-
"x-codex-parent-thread-id",
|
|
751
|
-
requestIdentity.parentThreadId,
|
|
752
|
-
);
|
|
753
|
-
}
|
|
754
|
-
if (requestIdentity?.subagentKind) {
|
|
755
|
-
setProviderGeneratedHeader(
|
|
756
|
-
headers,
|
|
757
|
-
"x-openai-subagent",
|
|
758
|
-
requestIdentity.subagentKind,
|
|
759
|
-
);
|
|
760
|
-
}
|
|
761
|
-
if (requestIdentity) {
|
|
762
|
-
setProviderGeneratedHeader(
|
|
763
|
-
headers,
|
|
764
|
-
"x-codex-turn-metadata",
|
|
765
|
-
buildCodexTurnMetadataJson(requestIdentity),
|
|
766
|
-
);
|
|
767
|
-
}
|
|
768
|
-
return headers;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
function clampReasoningEffort(modelId: string, effort: string): string {
|
|
772
|
-
const id = modelId.includes("/") ? (modelId.split("/").pop() ?? modelId) : modelId;
|
|
773
|
-
const gpt5MinorMatch = /^gpt-5\.(\d+)/.exec(id);
|
|
774
|
-
const gpt5Minor = gpt5MinorMatch ? Number.parseInt(gpt5MinorMatch[1], 10) : undefined;
|
|
775
|
-
if (gpt5Minor !== undefined && gpt5Minor >= 2 && effort === "minimal") return "low";
|
|
776
|
-
if (id === "gpt-5.1" && effort === "xhigh") return "high";
|
|
777
|
-
if (id === "gpt-5.1-codex-mini") return effort === "high" || effort === "xhigh" ? "high" : "medium";
|
|
778
|
-
return effort;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
const CODEX_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
782
|
-
type CodexThinkingLevel = (typeof CODEX_THINKING_LEVELS)[number];
|
|
783
|
-
|
|
784
|
-
/**
|
|
785
|
-
* Keep this local instead of delegating to older Pi releases: Pi added `max`
|
|
786
|
-
* after this extension's original minimum version, and an old clamp silently
|
|
787
|
-
* turns it into `off`.
|
|
788
|
-
*/
|
|
789
|
-
function clampCodexThinkingLevel(model: Model<Api>, level: ThinkingLevel): CodexThinkingLevel {
|
|
790
|
-
if (!model.reasoning) return "off";
|
|
791
|
-
const available = CODEX_THINKING_LEVELS.filter((candidate) => {
|
|
792
|
-
if (candidate === "off") return true;
|
|
793
|
-
const mapped = (model.thinkingLevelMap as Record<string, string | null | undefined> | undefined)?.[candidate];
|
|
794
|
-
if (mapped === null) return false;
|
|
795
|
-
if (candidate === "xhigh" || candidate === "max") return mapped !== undefined;
|
|
796
|
-
return true;
|
|
797
|
-
});
|
|
798
|
-
if (available.includes(level as CodexThinkingLevel)) return level as CodexThinkingLevel;
|
|
799
|
-
const requestedIndex = CODEX_THINKING_LEVELS.indexOf(level as CodexThinkingLevel);
|
|
800
|
-
if (requestedIndex < 0) return available[0] ?? "off";
|
|
801
|
-
for (let index = requestedIndex; index < CODEX_THINKING_LEVELS.length; index++) {
|
|
802
|
-
const candidate = CODEX_THINKING_LEVELS[index]!;
|
|
803
|
-
if (available.includes(candidate)) return candidate;
|
|
804
|
-
}
|
|
805
|
-
for (let index = requestedIndex - 1; index >= 0; index--) {
|
|
806
|
-
const candidate = CODEX_THINKING_LEVELS[index]!;
|
|
807
|
-
if (available.includes(candidate)) return candidate;
|
|
808
|
-
}
|
|
809
|
-
return available[0] ?? "off";
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
function thinkingLevelFromUnknown(value: unknown): ThinkingLevel | undefined {
|
|
813
|
-
return value === "minimal"
|
|
814
|
-
|| value === "low"
|
|
815
|
-
|| value === "medium"
|
|
816
|
-
|| value === "high"
|
|
817
|
-
|| value === "xhigh"
|
|
818
|
-
|| value === "max"
|
|
819
|
-
? value
|
|
820
|
-
: undefined;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
interface StartupPrewarmSnapshot {
|
|
824
|
-
systemPrompt: string;
|
|
825
|
-
tools: Context["tools"];
|
|
826
|
-
reasoning?: ThinkingLevel;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
function startupPrewarmSnapshot(pi: ExtensionAPI, ctx: any): StartupPrewarmSnapshot {
|
|
830
|
-
const activeToolNames = typeof pi.getActiveTools === "function" ? pi.getActiveTools() : [];
|
|
831
|
-
return {
|
|
832
|
-
systemPrompt: ctx.getSystemPrompt?.() ?? "",
|
|
833
|
-
tools: (typeof pi.getAllTools === "function" ? pi.getAllTools() : [])
|
|
834
|
-
.filter((tool) => activeToolNames.includes(tool.name))
|
|
835
|
-
.map((tool) => ({
|
|
836
|
-
name: tool.name,
|
|
837
|
-
description: tool.description,
|
|
838
|
-
parameters: tool.parameters,
|
|
839
|
-
})),
|
|
840
|
-
reasoning: thinkingLevelFromUnknown(
|
|
841
|
-
(ctx as { thinkingLevel?: unknown }).thinkingLevel
|
|
842
|
-
?? (typeof pi.getThinkingLevel === "function" ? pi.getThinkingLevel() : undefined),
|
|
843
|
-
),
|
|
844
|
-
};
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
function getServiceTierCostMultiplier(
|
|
848
|
-
model: Model<Api>,
|
|
849
|
-
serviceTier: ServiceTier,
|
|
850
|
-
cwd: string,
|
|
851
|
-
): number {
|
|
852
|
-
if (serviceTier === "flex") return 0.5;
|
|
853
|
-
const settings = loadModelSettings(model, cwd);
|
|
854
|
-
return serviceTier && serviceTier === settings.fastServiceTier
|
|
855
|
-
? settings.fastCostMultiplier ?? 1
|
|
856
|
-
: 1;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
function applyServiceTierPricing(
|
|
860
|
-
usage: AssistantMessage["usage"],
|
|
861
|
-
serviceTier: ServiceTier,
|
|
862
|
-
model: Model<Api>,
|
|
863
|
-
cwd: string,
|
|
864
|
-
): void {
|
|
865
|
-
const multiplier = getServiceTierCostMultiplier(model, serviceTier, cwd);
|
|
866
|
-
if (multiplier === 1) return;
|
|
867
|
-
usage.cost.input *= multiplier;
|
|
868
|
-
usage.cost.output *= multiplier;
|
|
869
|
-
usage.cost.cacheRead *= multiplier;
|
|
870
|
-
usage.cost.cacheWrite *= multiplier;
|
|
871
|
-
usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
function resolveCodexServiceTier(responseServiceTier: ServiceTier, requestServiceTier: ServiceTier): ServiceTier {
|
|
875
|
-
if (
|
|
876
|
-
responseServiceTier === "default"
|
|
877
|
-
&& (requestServiceTier === "flex" || requestServiceTier === "priority")
|
|
878
|
-
) {
|
|
879
|
-
return requestServiceTier;
|
|
880
|
-
}
|
|
881
|
-
return responseServiceTier ?? requestServiceTier;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
function withRequestServiceTier(
|
|
885
|
-
options: SimpleStreamOptions | undefined,
|
|
886
|
-
serviceTier: unknown,
|
|
887
|
-
): SimpleStreamOptions | undefined {
|
|
888
|
-
if (
|
|
889
|
-
serviceTier !== "auto"
|
|
890
|
-
&& serviceTier !== "default"
|
|
891
|
-
&& serviceTier !== "flex"
|
|
892
|
-
&& serviceTier !== "scale"
|
|
893
|
-
&& serviceTier !== "priority"
|
|
894
|
-
) {
|
|
895
|
-
return options;
|
|
896
|
-
}
|
|
897
|
-
return { ...options, serviceTier } as SimpleStreamOptions;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
function hasNativeWebSearchTool(body: ResponsesBody): boolean {
|
|
901
|
-
return Array.isArray(body.tools) && body.tools.some((tool) => Boolean(tool) && typeof tool === "object" && (tool as { type?: unknown }).type === "web_search");
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
function ensureWebSearchDetailsIncluded(body: ResponsesBody): void {
|
|
905
|
-
if (!hasNativeWebSearchTool(body)) return;
|
|
906
|
-
const include = Array.isArray(body.include) ? body.include : [];
|
|
907
|
-
const missing = [WEB_SEARCH_SOURCES_INCLUDE, WEB_SEARCH_RESULTS_INCLUDE].filter((value) => !include.includes(value));
|
|
908
|
-
if (missing.length > 0) body.include = [...include, ...missing];
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
function stripResponsesLiteImageDetails(value: unknown): void {
|
|
912
|
-
if (Array.isArray(value)) {
|
|
913
|
-
for (const item of value) stripResponsesLiteImageDetails(item);
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
if (!value || typeof value !== "object") return;
|
|
917
|
-
const record = value as Record<string, unknown>;
|
|
918
|
-
if (record.type === "input_image") delete record.detail;
|
|
919
|
-
for (const entry of Object.values(record)) stripResponsesLiteImageDetails(entry);
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
export function withResponsesLiteWebSocketMetadata<T extends { client_metadata?: Record<string, string> }>(body: T, responsesMode: CodexRequestProfile["responsesMode"]): T {
|
|
923
|
-
if (responsesMode !== "lite") return body;
|
|
924
|
-
return {
|
|
925
|
-
...body,
|
|
926
|
-
client_metadata: {
|
|
927
|
-
...body.client_metadata,
|
|
928
|
-
[WS_RESPONSES_LITE_CLIENT_METADATA_KEY]: "true",
|
|
929
|
-
},
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
/**
|
|
934
|
-
* Build the `x-codex-turn-metadata` compatibility blob the CLI sends inside
|
|
935
|
-
* `client_metadata` for every request kind. The gateway currently rewrites
|
|
936
|
-
* this blob (privacy normalization); once it honors client-supplied values,
|
|
937
|
-
* this keeps the shape identical to the CLI.
|
|
938
|
-
*/
|
|
939
|
-
function buildCodexTurnMetadataJson(identity: CodexRequestIdentity): string {
|
|
940
|
-
const payload: Record<string, string | number> = {
|
|
941
|
-
installation_id: identity.installationId,
|
|
942
|
-
session_id: identity.sessionId,
|
|
943
|
-
thread_id: identity.threadId,
|
|
944
|
-
turn_id: identity.turnId,
|
|
945
|
-
window_id: identity.windowId,
|
|
946
|
-
request_kind: identity.requestKind,
|
|
947
|
-
...(identity.turnStartedAtMs !== undefined
|
|
948
|
-
? { turn_started_at_unix_ms: identity.turnStartedAtMs }
|
|
949
|
-
: {}),
|
|
950
|
-
...(identity.agentName ? { agent_name: identity.agentName } : {}),
|
|
951
|
-
...(identity.forkedFromThreadId
|
|
952
|
-
? { forked_from_thread_id: identity.forkedFromThreadId }
|
|
953
|
-
: {}),
|
|
954
|
-
...(identity.parentThreadId
|
|
955
|
-
? { parent_thread_id: identity.parentThreadId }
|
|
956
|
-
: {}),
|
|
957
|
-
...(identity.parentTurnId
|
|
958
|
-
? { parent_turn_id: identity.parentTurnId }
|
|
959
|
-
: {}),
|
|
960
|
-
...(identity.rootTurnId
|
|
961
|
-
? { root_turn_id: identity.rootTurnId }
|
|
962
|
-
: {}),
|
|
963
|
-
...(identity.subagentKind
|
|
964
|
-
? { subagent_kind: identity.subagentKind }
|
|
965
|
-
: {}),
|
|
966
|
-
};
|
|
967
|
-
return JSON.stringify(payload);
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
function identityForRequestMetadata(
|
|
971
|
-
metadata: WebSocketRequestMetadata,
|
|
972
|
-
): CodexRequestIdentity | undefined {
|
|
973
|
-
if (metadata.identity) return metadata.identity;
|
|
974
|
-
const explicit: Record<string, unknown> = {
|
|
975
|
-
turn_id: metadata.turnId,
|
|
976
|
-
};
|
|
977
|
-
if (isUuidV7(metadata.threadId)) explicit.thread_id = metadata.threadId;
|
|
978
|
-
return resolveCodexRequestIdentity(
|
|
979
|
-
metadata.sessionId,
|
|
980
|
-
explicit,
|
|
981
|
-
metadata.requestKind ?? "turn",
|
|
982
|
-
);
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
/**
|
|
986
|
-
* Inject the Codex-compatible `client_metadata` for an SSE request, sourced
|
|
987
|
-
* from the same (session, thread, turn) metadata as the WebSocket path. The
|
|
988
|
-
* WebSocket-only fields (`x-codex-ws-stream-request-start-ms`, turn-state,
|
|
989
|
-
* Lite flag) stay out: SSE carries the Lite header and turn-state header.
|
|
990
|
-
*/
|
|
991
|
-
export function withSseRequestMetadata(body: ResponsesBody, metadata: WebSocketRequestMetadata): ResponsesBody {
|
|
992
|
-
const identity = identityForRequestMetadata(metadata);
|
|
993
|
-
if (!identity) return body;
|
|
994
|
-
const turnMetadata = buildCodexTurnMetadataJson(identity);
|
|
995
|
-
return {
|
|
996
|
-
...body,
|
|
997
|
-
client_metadata: {
|
|
998
|
-
...body.client_metadata,
|
|
999
|
-
session_id: identity.sessionId,
|
|
1000
|
-
thread_id: identity.threadId,
|
|
1001
|
-
"x-codex-window-id": identity.windowId,
|
|
1002
|
-
turn_id: identity.turnId,
|
|
1003
|
-
"x-codex-installation-id": identity.installationId,
|
|
1004
|
-
...(identity.parentThreadId
|
|
1005
|
-
? { "x-codex-parent-thread-id": identity.parentThreadId }
|
|
1006
|
-
: {}),
|
|
1007
|
-
...(identity.parentTurnId
|
|
1008
|
-
? { parent_turn_id: identity.parentTurnId }
|
|
1009
|
-
: {}),
|
|
1010
|
-
...(identity.rootTurnId
|
|
1011
|
-
? { root_turn_id: identity.rootTurnId }
|
|
1012
|
-
: {}),
|
|
1013
|
-
...(identity.subagentKind
|
|
1014
|
-
? { "x-openai-subagent": identity.subagentKind }
|
|
1015
|
-
: {}),
|
|
1016
|
-
...(turnMetadata ? { "x-codex-turn-metadata": turnMetadata } : {}),
|
|
1017
|
-
},
|
|
1018
|
-
};
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
function withWebSocketRequestMetadata(body: ResponsesBody, metadata: WebSocketRequestMetadata): ResponsesBody {
|
|
1022
|
-
const identity = identityForRequestMetadata(metadata);
|
|
1023
|
-
const turnMetadata = identity ? buildCodexTurnMetadataJson(identity) : "";
|
|
1024
|
-
return {
|
|
1025
|
-
...body,
|
|
1026
|
-
client_metadata: {
|
|
1027
|
-
...body.client_metadata,
|
|
1028
|
-
...(identity ? { session_id: identity.sessionId } : {}),
|
|
1029
|
-
...(identity ? { thread_id: identity.threadId } : {}),
|
|
1030
|
-
...(identity ? { "x-codex-window-id": identity.windowId } : {}),
|
|
1031
|
-
turn_id: identity?.turnId ?? metadata.turnId,
|
|
1032
|
-
...(identity
|
|
1033
|
-
? { "x-codex-installation-id": identity.installationId }
|
|
1034
|
-
: {}),
|
|
1035
|
-
...(identity?.parentThreadId
|
|
1036
|
-
? { "x-codex-parent-thread-id": identity.parentThreadId }
|
|
1037
|
-
: {}),
|
|
1038
|
-
...(identity?.parentTurnId
|
|
1039
|
-
? { parent_turn_id: identity.parentTurnId }
|
|
1040
|
-
: {}),
|
|
1041
|
-
...(identity?.rootTurnId
|
|
1042
|
-
? { root_turn_id: identity.rootTurnId }
|
|
1043
|
-
: {}),
|
|
1044
|
-
...(identity?.subagentKind
|
|
1045
|
-
? { "x-openai-subagent": identity.subagentKind }
|
|
1046
|
-
: {}),
|
|
1047
|
-
...(turnMetadata ? { "x-codex-turn-metadata": turnMetadata } : {}),
|
|
1048
|
-
...(identity?.turnState
|
|
1049
|
-
? { "x-codex-turn-state": identity.turnState }
|
|
1050
|
-
: {}),
|
|
1051
|
-
[WS_STREAM_REQUEST_START_MS_CLIENT_METADATA_KEY]: Date.now().toString(),
|
|
1052
|
-
},
|
|
1053
|
-
};
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
export function buildRequestBody<TApi extends Api>(model: Model<TApi>, context: Context, profile: CodexRequestProfile, options?: SimpleStreamOptions): ResponsesBody {
|
|
1057
|
-
const requestIdentity = resolveCodexRequestIdentity(
|
|
1058
|
-
options?.sessionId,
|
|
1059
|
-
options?.metadata as Record<string, unknown> | undefined,
|
|
1060
|
-
// Only the session-scoped prompt cache key is needed here. Do not
|
|
1061
|
-
// synthesize a logical turn while constructing startup/prewarm bodies.
|
|
1062
|
-
"prewarm",
|
|
1063
|
-
);
|
|
1064
|
-
const messages = convertResponsesMessages(model, context, new Set([...CODEX_TOOL_CALL_PROVIDERS, model.provider]), {
|
|
1065
|
-
includeSystemPrompt: false,
|
|
1066
|
-
});
|
|
1067
|
-
const tools = context.tools && context.tools.length > 0
|
|
1068
|
-
? convertResponsesTools(context.tools, { strict: null }).map((tool) =>
|
|
1069
|
-
profile.patchTransport === "custom" && tool.type === "function" && tool.name === "apply_patch" ? createCodexApplyPatchCustomTool() : tool)
|
|
1070
|
-
: [];
|
|
1071
|
-
const lite = profile.responsesMode === "lite";
|
|
1072
|
-
const liteTools = (): unknown[] => {
|
|
1073
|
-
const namespaces = new Map<string, {
|
|
1074
|
-
type: "namespace";
|
|
1075
|
-
name: string;
|
|
1076
|
-
description: string;
|
|
1077
|
-
tools: unknown[];
|
|
1078
|
-
}>();
|
|
1079
|
-
for (const tool of tools as Array<Record<string, unknown>>) {
|
|
1080
|
-
if (typeof tool.name !== "string") continue;
|
|
1081
|
-
if (tool.name === "web_search" || tool.name === "image_generation") {
|
|
1082
|
-
const reserved = createCodexReservedNamespaceTool(tool.name);
|
|
1083
|
-
namespaces.set(reserved.name, reserved);
|
|
1084
|
-
continue;
|
|
1085
|
-
}
|
|
1086
|
-
let namespace = namespaces.get("functions");
|
|
1087
|
-
if (!namespace) {
|
|
1088
|
-
namespace = {
|
|
1089
|
-
type: "namespace",
|
|
1090
|
-
name: "functions",
|
|
1091
|
-
description: "",
|
|
1092
|
-
tools: [],
|
|
1093
|
-
};
|
|
1094
|
-
namespaces.set("functions", namespace);
|
|
1095
|
-
}
|
|
1096
|
-
const nestedTool: Record<string, unknown> = { ...tool };
|
|
1097
|
-
if (nestedTool.type === "function" && typeof nestedTool.strict !== "boolean") {
|
|
1098
|
-
nestedTool.strict = false;
|
|
1099
|
-
}
|
|
1100
|
-
namespace.tools.push(nestedTool);
|
|
1101
|
-
}
|
|
1102
|
-
return [...namespaces.values()].filter((namespace) => namespace.tools.length > 0);
|
|
1103
|
-
};
|
|
1104
|
-
|
|
1105
|
-
const body: ResponsesBody = {
|
|
1106
|
-
model: model.id,
|
|
1107
|
-
store: false,
|
|
1108
|
-
stream: true,
|
|
1109
|
-
input: [],
|
|
1110
|
-
text: { verbosity: ((options as { textVerbosity?: string } | undefined)?.textVerbosity ?? "low") as string },
|
|
1111
|
-
include: ["reasoning.encrypted_content"],
|
|
1112
|
-
prompt_cache_key: requestIdentity?.sessionId ?? options?.sessionId,
|
|
1113
|
-
tool_choice: "auto",
|
|
1114
|
-
parallel_tool_calls: profile.supportsParallelTools,
|
|
1115
|
-
};
|
|
1116
|
-
if (lite) {
|
|
1117
|
-
stripResponsesLiteImageDetails(messages);
|
|
1118
|
-
body.input = [
|
|
1119
|
-
{ type: "additional_tools", role: "developer", tools: liteTools() },
|
|
1120
|
-
...(context.systemPrompt
|
|
1121
|
-
? [{ type: "message", role: "developer", content: [{ type: "input_text", text: context.systemPrompt }] }]
|
|
1122
|
-
: []),
|
|
1123
|
-
...messages,
|
|
1124
|
-
];
|
|
1125
|
-
body.reasoning = { context: "all_turns" };
|
|
1126
|
-
} else {
|
|
1127
|
-
if (profile.systemPromptPlacement === "instructions") {
|
|
1128
|
-
body.instructions = context.systemPrompt;
|
|
1129
|
-
body.input = messages;
|
|
1130
|
-
} else {
|
|
1131
|
-
body.input = [
|
|
1132
|
-
...(context.systemPrompt
|
|
1133
|
-
? [{ type: "message", role: "developer", content: [{ type: "input_text", text: context.systemPrompt }] }]
|
|
1134
|
-
: []),
|
|
1135
|
-
...messages,
|
|
1136
|
-
];
|
|
1137
|
-
}
|
|
1138
|
-
if (tools.length > 0) body.tools = tools;
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
// The Codex ChatGPT-backed endpoint rejects output-token cap fields with
|
|
1142
|
-
// `Unsupported parameter: max_output_tokens`. Pi's branch summarizer passes
|
|
1143
|
-
// `maxTokens`, so forwarding it breaks `/tree` summaries and extensions that
|
|
1144
|
-
// use `ctx.navigateTree(..., { summarize: true })`.
|
|
1145
|
-
|
|
1146
|
-
if ((options as { temperature?: number } | undefined)?.temperature !== undefined) {
|
|
1147
|
-
body.temperature = (options as { temperature?: number }).temperature;
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
const serviceTier = (options as { serviceTier?: string } | undefined)?.serviceTier;
|
|
1151
|
-
if (serviceTier !== undefined) {
|
|
1152
|
-
body.service_tier = serviceTier;
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
const clampedReasoning = options?.reasoning
|
|
1156
|
-
? clampCodexThinkingLevel(model as Model<Api>, options.reasoning)
|
|
1157
|
-
: undefined;
|
|
1158
|
-
const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
|
|
1159
|
-
if (reasoningEffort !== undefined) {
|
|
1160
|
-
const effort = model.thinkingLevelMap?.[reasoningEffort] ?? reasoningEffort;
|
|
1161
|
-
if (effort === null) return body;
|
|
1162
|
-
const reasoning = body.reasoning ?? {};
|
|
1163
|
-
reasoning.effort = clampReasoningEffort(model.id, effort);
|
|
1164
|
-
const summary = (options as { reasoningSummary?: ReasoningSummary | null } | undefined)?.reasoningSummary
|
|
1165
|
-
?? profile.reasoningSummary;
|
|
1166
|
-
if (summary && summary !== "none") reasoning.summary = summary;
|
|
1167
|
-
body.reasoning = reasoning;
|
|
1168
|
-
} else if (lite && clampedReasoning !== "off") {
|
|
1169
|
-
// Match the Codex CLI default reasoning level for Lite models: the CLI
|
|
1170
|
-
// always sends `reasoning.effort` (default "low") for gpt-5.6-* models.
|
|
1171
|
-
const reasoning = body.reasoning ?? {};
|
|
1172
|
-
if (reasoning.effort === undefined) {
|
|
1173
|
-
reasoning.effort = model.thinkingLevelMap?.low ?? "low";
|
|
1174
|
-
}
|
|
1175
|
-
body.reasoning = reasoning;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
return body;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
function isRetryableError(status: number, errorText: string): boolean {
|
|
1182
|
-
if (status === 429 || status === 500 || status === 502 || status === 503 || status === 504) {
|
|
1183
|
-
return true;
|
|
1184
|
-
}
|
|
1185
|
-
return /rate.?limit|overloaded|service.?unavailable|upstream.?connect|connection.?refused/i.test(errorText);
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
export function withHttpStatusPrefix(status: number, message: string): string {
|
|
1189
|
-
const trimmed = message.trim() || "Request failed";
|
|
1190
|
-
if (HTTP_STATUS_MESSAGE_PREFIX.test(trimmed)) return trimmed;
|
|
1191
|
-
return `HTTP ${status}: ${trimmed}`;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
function sleep(ms: number, signal: AbortSignal | undefined): Promise<void> {
|
|
1195
|
-
return new Promise((resolve, reject) => {
|
|
1196
|
-
if (signal?.aborted) {
|
|
1197
|
-
reject(new Error("Request was aborted"));
|
|
1198
|
-
return;
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
const timeout = setTimeout(resolve, ms);
|
|
1202
|
-
signal?.addEventListener(
|
|
1203
|
-
"abort",
|
|
1204
|
-
() => {
|
|
1205
|
-
clearTimeout(timeout);
|
|
1206
|
-
reject(new Error("Request was aborted"));
|
|
1207
|
-
},
|
|
1208
|
-
{ once: true },
|
|
1209
|
-
);
|
|
1210
|
-
});
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
export function responseHeaderTimeoutMsFromOptions(options: SimpleStreamOptions | undefined): number {
|
|
1214
|
-
const value = (options as { timeoutMs?: unknown } | undefined)?.timeoutMs;
|
|
1215
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : SSE_RESPONSE_HEADER_TIMEOUT_MS;
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
export async function fetchWithResponseHeaderTimeout(
|
|
1219
|
-
url: string,
|
|
1220
|
-
init: RequestInit,
|
|
1221
|
-
parentSignal: AbortSignal | undefined,
|
|
1222
|
-
timeoutMs = SSE_RESPONSE_HEADER_TIMEOUT_MS,
|
|
1223
|
-
): Promise<Response> {
|
|
1224
|
-
if (parentSignal?.aborted) throw new Error("Request was aborted");
|
|
1225
|
-
|
|
1226
|
-
const controller = new AbortController();
|
|
1227
|
-
let timedOut = false;
|
|
1228
|
-
let parentAborted = false;
|
|
1229
|
-
const timeoutMessage = `Codex Responses SSE response headers timed out after ${timeoutMs}ms`;
|
|
1230
|
-
|
|
1231
|
-
const onParentAbort = () => {
|
|
1232
|
-
parentAborted = true;
|
|
1233
|
-
controller.abort(parentSignal?.reason);
|
|
1234
|
-
};
|
|
1235
|
-
|
|
1236
|
-
if (parentSignal) parentSignal.addEventListener("abort", onParentAbort, { once: true });
|
|
1237
|
-
const timeout = setTimeout(() => {
|
|
1238
|
-
timedOut = true;
|
|
1239
|
-
controller.abort(new Error(timeoutMessage));
|
|
1240
|
-
}, Math.max(1, timeoutMs));
|
|
1241
|
-
|
|
1242
|
-
try {
|
|
1243
|
-
return await fetch(url, { ...init, signal: controller.signal });
|
|
1244
|
-
} catch (error) {
|
|
1245
|
-
if (timedOut) throw new Error(timeoutMessage);
|
|
1246
|
-
if (parentAborted || parentSignal?.aborted) throw new Error("Request was aborted");
|
|
1247
|
-
throw error;
|
|
1248
|
-
} finally {
|
|
1249
|
-
clearTimeout(timeout);
|
|
1250
|
-
if (parentSignal) parentSignal.removeEventListener("abort", onParentAbort);
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
async function* parseSSE(response: Response): AsyncIterable<StreamEventShape> {
|
|
1255
|
-
if (!response.body) return;
|
|
1256
|
-
|
|
1257
|
-
const reader = response.body.getReader();
|
|
1258
|
-
const decoder = new TextDecoder();
|
|
1259
|
-
let buffer = "";
|
|
1260
|
-
|
|
1261
|
-
try {
|
|
1262
|
-
while (true) {
|
|
1263
|
-
const { done, value } = await reader.read();
|
|
1264
|
-
if (done) break;
|
|
1265
|
-
|
|
1266
|
-
buffer += decoder.decode(value, { stream: true });
|
|
1267
|
-
buffer = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1268
|
-
let idx = buffer.indexOf("\n\n");
|
|
1269
|
-
while (idx !== -1) {
|
|
1270
|
-
const chunk = buffer.slice(0, idx);
|
|
1271
|
-
buffer = buffer.slice(idx + 2);
|
|
1272
|
-
const dataLines = chunk
|
|
1273
|
-
.split("\n")
|
|
1274
|
-
.filter((line) => line.startsWith("data:"))
|
|
1275
|
-
.map((line) => line.slice(5).trim());
|
|
1276
|
-
if (dataLines.length > 0) {
|
|
1277
|
-
const data = dataLines.join("\n").trim();
|
|
1278
|
-
if (data && data !== "[DONE]") {
|
|
1279
|
-
try {
|
|
1280
|
-
yield JSON.parse(data) as StreamEventShape;
|
|
1281
|
-
} catch {
|
|
1282
|
-
// Ignore malformed SSE chunks and continue consuming the stream.
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
idx = buffer.indexOf("\n\n");
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
} finally {
|
|
1290
|
-
try {
|
|
1291
|
-
await reader.cancel();
|
|
1292
|
-
} catch {
|
|
1293
|
-
// ignore cancellation errors
|
|
1294
|
-
}
|
|
1295
|
-
try {
|
|
1296
|
-
reader.releaseLock();
|
|
1297
|
-
} catch {
|
|
1298
|
-
// ignore lock release errors
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
function envFirst(names: string[]): string | undefined {
|
|
1304
|
-
if (typeof process === "undefined") return undefined;
|
|
1305
|
-
for (const name of names) {
|
|
1306
|
-
const value = process.env[name];
|
|
1307
|
-
if (value?.trim()) return value.trim();
|
|
1308
|
-
}
|
|
1309
|
-
return undefined;
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
function noProxyMatches(hostname: string, noProxy: string | undefined): boolean {
|
|
1313
|
-
if (!noProxy) return false;
|
|
1314
|
-
const host = hostname.toLowerCase();
|
|
1315
|
-
for (const rawPart of noProxy.split(",")) {
|
|
1316
|
-
const part = rawPart.trim().toLowerCase();
|
|
1317
|
-
if (!part) continue;
|
|
1318
|
-
if (part === "*") return true;
|
|
1319
|
-
const normalized = part.startsWith(".") ? part.slice(1) : part;
|
|
1320
|
-
if (host === normalized || host.endsWith(`.${normalized}`)) return true;
|
|
1321
|
-
}
|
|
1322
|
-
return false;
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
export function proxyForWebSocketUrl(rawUrl: string): string | undefined {
|
|
1326
|
-
let url: URL;
|
|
1327
|
-
try {
|
|
1328
|
-
url = new URL(rawUrl);
|
|
1329
|
-
} catch {
|
|
1330
|
-
return undefined;
|
|
1331
|
-
}
|
|
1332
|
-
const noProxy = envFirst(["NO_PROXY", "no_proxy"]);
|
|
1333
|
-
if (noProxyMatches(url.hostname, noProxy)) return undefined;
|
|
1334
|
-
if (url.protocol === "wss:" || url.protocol === "https:") {
|
|
1335
|
-
return envFirst(["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy"]);
|
|
1336
|
-
}
|
|
1337
|
-
if (url.protocol === "ws:" || url.protocol === "http:") {
|
|
1338
|
-
return envFirst(["HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy", "ALL_PROXY", "all_proxy"]);
|
|
1339
|
-
}
|
|
1340
|
-
return undefined;
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
async function proxyDispatcherForUrl(rawUrl: string): Promise<Dispatcher | undefined> {
|
|
1344
|
-
const proxy = proxyForWebSocketUrl(rawUrl);
|
|
1345
|
-
if (!proxy) return undefined;
|
|
1346
|
-
return new ProxyAgent(proxy);
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
export async function webSocketOptionsForUrl(url: string, headers: Record<string, string>): Promise<{
|
|
1350
|
-
headers: Record<string, string>;
|
|
1351
|
-
dispatcher?: Dispatcher;
|
|
1352
|
-
}> {
|
|
1353
|
-
const dispatcher = await proxyDispatcherForUrl(url);
|
|
1354
|
-
return dispatcher ? { headers, dispatcher } : { headers };
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
function getWebSocketReadyState(socket: WebSocketLike): number | undefined {
|
|
1358
|
-
return typeof socket.readyState === "number" ? socket.readyState : undefined;
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
function isWebSocketReusable(socket: WebSocketLike): boolean {
|
|
1362
|
-
const readyState = getWebSocketReadyState(socket);
|
|
1363
|
-
return readyState === undefined || readyState === 1;
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
function closeWebSocketSilently(socket: WebSocketLike, code = 1000, reason = "done"): void {
|
|
1367
|
-
try {
|
|
1368
|
-
socket.close(code, reason);
|
|
1369
|
-
} catch {
|
|
1370
|
-
// ignore close errors
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
export function closeProviderWebSocketSessions(sessionId?: string): void {
|
|
1375
|
-
for (const cacheKey of websocketConnectionPromises.keys()) {
|
|
1376
|
-
if (sessionId && !cacheKey.startsWith(`${sessionId}\n`)) continue;
|
|
1377
|
-
websocketConnectionPromises.delete(cacheKey);
|
|
1378
|
-
}
|
|
1379
|
-
for (const [cacheKey, entry] of websocketSessionCache) {
|
|
1380
|
-
if (sessionId && !cacheKey.startsWith(`${sessionId}\n`)) continue;
|
|
1381
|
-
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
1382
|
-
for (const waiter of entry.waiters.splice(0)) {
|
|
1383
|
-
if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
1384
|
-
waiter.reject(new Error("WebSocket session closed"));
|
|
1385
|
-
}
|
|
1386
|
-
closeWebSocketSilently(entry.socket, 1000, "session_shutdown");
|
|
1387
|
-
websocketSessionCache.delete(cacheKey);
|
|
1388
|
-
}
|
|
1389
|
-
if (sessionId) {
|
|
1390
|
-
for (const fallbackKey of websocketHttpFallbackSessions) {
|
|
1391
|
-
if (fallbackKey.startsWith(`${sessionId}\n`)) websocketHttpFallbackSessions.delete(fallbackKey);
|
|
1392
|
-
}
|
|
1393
|
-
} else {
|
|
1394
|
-
websocketHttpFallbackSessions.clear();
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
function scheduleSessionWebSocketExpiry(cacheKey: string, entry: SessionWebSocketCacheEntry): void {
|
|
1400
|
-
if (entry.idleTimer) {
|
|
1401
|
-
clearTimeout(entry.idleTimer);
|
|
1402
|
-
}
|
|
1403
|
-
entry.idleTimer = setTimeout(() => {
|
|
1404
|
-
if (entry.busy || entry.waiters.length > 0) return;
|
|
1405
|
-
closeWebSocketSilently(entry.socket, 1000, "idle_timeout");
|
|
1406
|
-
websocketSessionCache.delete(cacheKey);
|
|
1407
|
-
}, SESSION_WEBSOCKET_CACHE_TTL_MS);
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
function removeWebSocketWaiter(entry: SessionWebSocketCacheEntry, waiter: WebSocketAcquireWaiter): void {
|
|
1411
|
-
const index = entry.waiters.indexOf(waiter);
|
|
1412
|
-
if (index >= 0) entry.waiters.splice(index, 1);
|
|
1413
|
-
if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
function acquireCachedWebSocketEntry(
|
|
1417
|
-
cacheKey: string,
|
|
1418
|
-
entry: SessionWebSocketCacheEntry,
|
|
1419
|
-
reused: boolean,
|
|
1420
|
-
): AcquiredWebSocket {
|
|
1421
|
-
entry.busy = true;
|
|
1422
|
-
let released = false;
|
|
1423
|
-
const release = ({ keep } = {} as { keep?: boolean }) => {
|
|
1424
|
-
if (released) return;
|
|
1425
|
-
const reusable = keep !== false && isWebSocketReusable(entry.socket);
|
|
1426
|
-
if (!reusable) {
|
|
1427
|
-
released = true;
|
|
1428
|
-
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
1429
|
-
closeWebSocketSilently(entry.socket);
|
|
1430
|
-
if (websocketSessionCache.get(cacheKey) === entry) {
|
|
1431
|
-
websocketSessionCache.delete(cacheKey);
|
|
1432
|
-
}
|
|
1433
|
-
for (const waiter of entry.waiters.splice(0)) {
|
|
1434
|
-
if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
1435
|
-
waiter.reject(new Error("WebSocket connection became unavailable"));
|
|
1436
|
-
}
|
|
1437
|
-
return;
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
while (entry.waiters.length > 0) {
|
|
1441
|
-
const waiter = entry.waiters.shift()!;
|
|
1442
|
-
if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener("abort", waiter.onAbort);
|
|
1443
|
-
if (waiter.signal?.aborted) {
|
|
1444
|
-
waiter.reject(new Error("Request was aborted"));
|
|
1445
|
-
continue;
|
|
1446
|
-
}
|
|
1447
|
-
released = true;
|
|
1448
|
-
waiter.resolve(acquireCachedWebSocketEntry(cacheKey, entry, true));
|
|
1449
|
-
return;
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
entry.busy = false;
|
|
1453
|
-
released = true;
|
|
1454
|
-
scheduleSessionWebSocketExpiry(cacheKey, entry);
|
|
1455
|
-
};
|
|
1456
|
-
return {
|
|
1457
|
-
socket: entry.socket,
|
|
1458
|
-
entry,
|
|
1459
|
-
reused,
|
|
1460
|
-
release,
|
|
1461
|
-
};
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
async function waitForCachedWebSocket(
|
|
1465
|
-
cacheKey: string,
|
|
1466
|
-
entry: SessionWebSocketCacheEntry,
|
|
1467
|
-
signal: AbortSignal | undefined,
|
|
1468
|
-
): Promise<AcquiredWebSocket> {
|
|
1469
|
-
if (signal?.aborted) throw new Error("Request was aborted");
|
|
1470
|
-
return new Promise<AcquiredWebSocket>((resolve, reject) => {
|
|
1471
|
-
const waiter: WebSocketAcquireWaiter = {
|
|
1472
|
-
resolve,
|
|
1473
|
-
reject,
|
|
1474
|
-
...(signal ? { signal } : {}),
|
|
1475
|
-
};
|
|
1476
|
-
if (signal) {
|
|
1477
|
-
waiter.onAbort = () => {
|
|
1478
|
-
removeWebSocketWaiter(entry, waiter);
|
|
1479
|
-
reject(new Error("Request was aborted"));
|
|
1480
|
-
};
|
|
1481
|
-
signal.addEventListener("abort", waiter.onAbort, { once: true });
|
|
1482
|
-
}
|
|
1483
|
-
entry.waiters.push(waiter);
|
|
1484
|
-
});
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
function extractWebSocketError(event: unknown): Error {
|
|
1488
|
-
if (event && typeof event === "object") {
|
|
1489
|
-
const message = "message" in event ? (event as { message?: unknown }).message : undefined;
|
|
1490
|
-
if (typeof message === "string" && message.length > 0) {
|
|
1491
|
-
return new Error(message);
|
|
1492
|
-
}
|
|
1493
|
-
const nestedError = "error" in event ? (event as { error?: unknown }).error : undefined;
|
|
1494
|
-
if (nestedError instanceof Error && nestedError.message.length > 0) {
|
|
1495
|
-
return nestedError;
|
|
1496
|
-
}
|
|
1497
|
-
if (nestedError && typeof nestedError === "object" && "message" in nestedError) {
|
|
1498
|
-
const nestedMessage = (nestedError as { message?: unknown }).message;
|
|
1499
|
-
if (typeof nestedMessage === "string" && nestedMessage.length > 0) {
|
|
1500
|
-
return new Error(nestedMessage);
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
return new Error("WebSocket error");
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
function extractWebSocketCloseError(event: unknown): Error {
|
|
1508
|
-
if (event && typeof event === "object") {
|
|
1509
|
-
const code = "code" in event ? (event as { code?: unknown }).code : undefined;
|
|
1510
|
-
const reason = "reason" in event ? (event as { reason?: unknown }).reason : undefined;
|
|
1511
|
-
const codeText = typeof code === "number" ? ` ${code}` : "";
|
|
1512
|
-
const reasonText = typeof reason === "string" && reason.length > 0 ? ` ${reason}` : "";
|
|
1513
|
-
return new Error(`WebSocket closed${codeText}${reasonText}`.trim());
|
|
1514
|
-
}
|
|
1515
|
-
return new Error("WebSocket closed");
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
async function loadNodeWebSocketModule(): Promise<NodeWebSocketModule> {
|
|
1519
|
-
if (!nodeWebSocketModulePromise) {
|
|
1520
|
-
nodeWebSocketModulePromise = dynamicImport("ws") as Promise<NodeWebSocketModule>;
|
|
1521
|
-
}
|
|
1522
|
-
return nodeWebSocketModulePromise;
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
function nodeWebSocketHeaders(headers: Headers): Record<string, string> {
|
|
1526
|
-
return Object.fromEntries(headers.entries());
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
function nodeWebSocketResponseHeaders(headers: unknown): Record<string, string> {
|
|
1530
|
-
if (!headers || typeof headers !== "object") return {};
|
|
1531
|
-
const result: Record<string, string> = {};
|
|
1532
|
-
for (const [name, value] of Object.entries(headers as Record<string, unknown>)) {
|
|
1533
|
-
if (Array.isArray(value)) result[name] = value.join(", ");
|
|
1534
|
-
else if (typeof value === "string") result[name] = value;
|
|
1535
|
-
else if (value !== undefined) result[name] = String(value);
|
|
1536
|
-
}
|
|
1537
|
-
return result;
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
function handshakeMessage(status: number, statusText: string | undefined, body: string): string {
|
|
1541
|
-
const trimmedBody = body.trim();
|
|
1542
|
-
if (trimmedBody) {
|
|
1543
|
-
try {
|
|
1544
|
-
const parsed = JSON.parse(trimmedBody) as { error?: { message?: unknown }; message?: unknown };
|
|
1545
|
-
const message = typeof parsed.error?.message === "string"
|
|
1546
|
-
? parsed.error.message
|
|
1547
|
-
: typeof parsed.message === "string"
|
|
1548
|
-
? parsed.message
|
|
1549
|
-
: undefined;
|
|
1550
|
-
if (message?.trim()) return message.trim();
|
|
1551
|
-
} catch {
|
|
1552
|
-
return trimmedBody;
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
return statusText?.trim() || "WebSocket upgrade failed";
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
async function connectWebSocket(
|
|
1559
|
-
url: string,
|
|
1560
|
-
headers: Headers,
|
|
1561
|
-
signal: AbortSignal | undefined,
|
|
1562
|
-
timeoutMs = WEBSOCKET_CONNECT_TIMEOUT_MS,
|
|
1563
|
-
): Promise<WebSocketLike> {
|
|
1564
|
-
if (signal?.aborted) throw new Error("Request was aborted");
|
|
1565
|
-
const { WebSocket } = await loadNodeWebSocketModule();
|
|
1566
|
-
const proxy = proxyForWebSocketUrl(url);
|
|
1567
|
-
let agent: unknown;
|
|
1568
|
-
if (proxy) {
|
|
1569
|
-
const protocol = new URL(proxy).protocol.toLowerCase();
|
|
1570
|
-
if (protocol === "http:" || protocol === "https:") {
|
|
1571
|
-
const { HttpsProxyAgent } = await dynamicImport("https-proxy-agent") as typeof import("https-proxy-agent");
|
|
1572
|
-
agent = new HttpsProxyAgent(proxy);
|
|
1573
|
-
} else if (protocol === "socks:" || protocol === "socks4:" || protocol === "socks4a:" || protocol === "socks5:" || protocol === "socks5h:") {
|
|
1574
|
-
const { SocksProxyAgent } = await dynamicImport("socks-proxy-agent") as {
|
|
1575
|
-
SocksProxyAgent: new (proxy: string) => unknown;
|
|
1576
|
-
};
|
|
1577
|
-
agent = new SocksProxyAgent(proxy);
|
|
1578
|
-
} else {
|
|
1579
|
-
throw new Error(`Unsupported WebSocket proxy protocol: ${protocol}`);
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
return new Promise((resolve, reject) => {
|
|
1584
|
-
let settled = false;
|
|
1585
|
-
let socket: InstanceType<NodeWebSocketModule["WebSocket"]>;
|
|
1586
|
-
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
1587
|
-
|
|
1588
|
-
try {
|
|
1589
|
-
socket = new WebSocket(url, {
|
|
1590
|
-
headers: nodeWebSocketHeaders(headers),
|
|
1591
|
-
perMessageDeflate: true,
|
|
1592
|
-
...(agent ? { agent } : {}),
|
|
1593
|
-
});
|
|
1594
|
-
} catch (error) {
|
|
1595
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1596
|
-
return;
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
const onOpen = () => {
|
|
1600
|
-
if (settled) return;
|
|
1601
|
-
settled = true;
|
|
1602
|
-
cleanup();
|
|
1603
|
-
// Keep an error listener installed while the socket sits idle in the
|
|
1604
|
-
// session cache. Request parsers add their own listener, but Node's ws
|
|
1605
|
-
// EventEmitter would otherwise treat an idle "error" as uncaught.
|
|
1606
|
-
socket.on("error", () => {});
|
|
1607
|
-
const messageListeners = new Map<(event: unknown) => void, (...args: any[]) => void>();
|
|
1608
|
-
const closeListeners = new Map<(event: unknown) => void, (...args: any[]) => void>();
|
|
1609
|
-
resolve({
|
|
1610
|
-
get readyState() {
|
|
1611
|
-
return socket.readyState;
|
|
1612
|
-
},
|
|
1613
|
-
get bufferedAmount() {
|
|
1614
|
-
return socket.bufferedAmount;
|
|
1615
|
-
},
|
|
1616
|
-
send(data, callback) {
|
|
1617
|
-
socket.send(data, callback);
|
|
1618
|
-
},
|
|
1619
|
-
close(code, reason) {
|
|
1620
|
-
socket.close(code, reason);
|
|
1621
|
-
},
|
|
1622
|
-
addEventListener(type, listener) {
|
|
1623
|
-
if (type === "message") {
|
|
1624
|
-
const wrapped = (data: unknown, isBinary: boolean) => listener({ data, isBinary });
|
|
1625
|
-
messageListeners.set(listener, wrapped);
|
|
1626
|
-
socket.on("message", wrapped);
|
|
1627
|
-
return;
|
|
1628
|
-
}
|
|
1629
|
-
if (type === "close") {
|
|
1630
|
-
const wrapped = (code: number, reason: Buffer) => listener({
|
|
1631
|
-
code,
|
|
1632
|
-
reason: reason.toString("utf8"),
|
|
1633
|
-
});
|
|
1634
|
-
closeListeners.set(listener, wrapped);
|
|
1635
|
-
socket.on("close", wrapped);
|
|
1636
|
-
return;
|
|
1637
|
-
}
|
|
1638
|
-
socket.on(type, listener as (...args: any[]) => void);
|
|
1639
|
-
},
|
|
1640
|
-
removeEventListener(type, listener) {
|
|
1641
|
-
if (type === "message") {
|
|
1642
|
-
const wrapped = messageListeners.get(listener);
|
|
1643
|
-
if (wrapped) socket.off("message", wrapped);
|
|
1644
|
-
messageListeners.delete(listener);
|
|
1645
|
-
return;
|
|
1646
|
-
}
|
|
1647
|
-
if (type === "close") {
|
|
1648
|
-
const wrapped = closeListeners.get(listener);
|
|
1649
|
-
if (wrapped) socket.off("close", wrapped);
|
|
1650
|
-
closeListeners.delete(listener);
|
|
1651
|
-
return;
|
|
1652
|
-
}
|
|
1653
|
-
socket.off(type, listener as (...args: any[]) => void);
|
|
1654
|
-
},
|
|
1655
|
-
});
|
|
1656
|
-
};
|
|
1657
|
-
const onError = (event: unknown) => {
|
|
1658
|
-
if (settled) return;
|
|
1659
|
-
settled = true;
|
|
1660
|
-
cleanup();
|
|
1661
|
-
reject(event instanceof Error ? event : extractWebSocketError(event));
|
|
1662
|
-
};
|
|
1663
|
-
const onClose = (code: number, reason: Buffer) => {
|
|
1664
|
-
if (settled) return;
|
|
1665
|
-
settled = true;
|
|
1666
|
-
cleanup();
|
|
1667
|
-
reject(extractWebSocketCloseError({ code, reason: reason.toString("utf8") }));
|
|
1668
|
-
};
|
|
1669
|
-
const onUnexpectedResponse = (
|
|
1670
|
-
_request: unknown,
|
|
1671
|
-
response: { statusCode?: number; statusMessage?: string; headers?: unknown; on(type: string, listener: (...args: any[]) => void): void },
|
|
1672
|
-
) => {
|
|
1673
|
-
if (settled) return;
|
|
1674
|
-
let body = "";
|
|
1675
|
-
response.on("data", (chunk: unknown) => {
|
|
1676
|
-
if (body.length >= 64 * 1024) return;
|
|
1677
|
-
body += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
|
1678
|
-
});
|
|
1679
|
-
response.on("end", () => {
|
|
1680
|
-
if (settled) return;
|
|
1681
|
-
settled = true;
|
|
1682
|
-
cleanup();
|
|
1683
|
-
const status = response.statusCode ?? 500;
|
|
1684
|
-
reject(new WebSocketHandshakeError(
|
|
1685
|
-
status,
|
|
1686
|
-
handshakeMessage(status, response.statusMessage, body),
|
|
1687
|
-
nodeWebSocketResponseHeaders(response.headers),
|
|
1688
|
-
body || undefined,
|
|
1689
|
-
));
|
|
1690
|
-
});
|
|
1691
|
-
};
|
|
1692
|
-
const onAbort = () => {
|
|
1693
|
-
if (settled) return;
|
|
1694
|
-
settled = true;
|
|
1695
|
-
cleanup();
|
|
1696
|
-
socket.on("error", () => {});
|
|
1697
|
-
socket.terminate?.();
|
|
1698
|
-
reject(new Error("Request was aborted"));
|
|
1699
|
-
};
|
|
1700
|
-
const onTimeout = () => {
|
|
1701
|
-
if (settled) return;
|
|
1702
|
-
settled = true;
|
|
1703
|
-
cleanup();
|
|
1704
|
-
socket.on("error", () => {});
|
|
1705
|
-
socket.terminate?.();
|
|
1706
|
-
reject(new Error(`OpenAI Responses WebSocket connection timed out after ${timeoutMs}ms`));
|
|
1707
|
-
};
|
|
1708
|
-
|
|
1709
|
-
const cleanup = () => {
|
|
1710
|
-
if (timeout) clearTimeout(timeout);
|
|
1711
|
-
socket.off("open", onOpen);
|
|
1712
|
-
socket.off("error", onError);
|
|
1713
|
-
socket.off("close", onClose);
|
|
1714
|
-
socket.off("unexpected-response", onUnexpectedResponse);
|
|
1715
|
-
signal?.removeEventListener("abort", onAbort);
|
|
1716
|
-
};
|
|
1717
|
-
|
|
1718
|
-
socket.on("open", onOpen);
|
|
1719
|
-
socket.on("error", onError);
|
|
1720
|
-
socket.on("close", onClose);
|
|
1721
|
-
socket.on("unexpected-response", onUnexpectedResponse);
|
|
1722
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1723
|
-
timeout = setTimeout(onTimeout, Math.max(1, timeoutMs));
|
|
1724
|
-
});
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
async function acquireWebSocket(
|
|
1728
|
-
url: string,
|
|
1729
|
-
headers: Headers,
|
|
1730
|
-
cacheKey: string | undefined,
|
|
1731
|
-
sessionId: string | undefined,
|
|
1732
|
-
signal: AbortSignal | undefined,
|
|
1733
|
-
connectTimeoutMs: number,
|
|
1734
|
-
): Promise<AcquiredWebSocket> {
|
|
1735
|
-
if (!cacheKey || !sessionId) {
|
|
1736
|
-
const socket = await connectWebSocket(url, headers, signal, connectTimeoutMs);
|
|
1737
|
-
return {
|
|
1738
|
-
socket,
|
|
1739
|
-
reused: false,
|
|
1740
|
-
release: ({ keep } = {}) => {
|
|
1741
|
-
if (keep === false) {
|
|
1742
|
-
closeWebSocketSilently(socket);
|
|
1743
|
-
return;
|
|
1744
|
-
}
|
|
1745
|
-
closeWebSocketSilently(socket);
|
|
1746
|
-
},
|
|
1747
|
-
};
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
const cached = websocketSessionCache.get(cacheKey);
|
|
1751
|
-
if (cached) {
|
|
1752
|
-
if (cached.idleTimer) {
|
|
1753
|
-
clearTimeout(cached.idleTimer);
|
|
1754
|
-
cached.idleTimer = undefined;
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
if (!cached.busy && isWebSocketReusable(cached.socket)) {
|
|
1758
|
-
return acquireCachedWebSocketEntry(cacheKey, cached, true);
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
if (cached.busy) {
|
|
1762
|
-
return waitForCachedWebSocket(cacheKey, cached, signal);
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
if (!isWebSocketReusable(cached.socket)) {
|
|
1766
|
-
closeWebSocketSilently(cached.socket);
|
|
1767
|
-
websocketSessionCache.delete(cacheKey);
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
let pendingConnection = websocketConnectionPromises.get(cacheKey);
|
|
1772
|
-
if (!pendingConnection) {
|
|
1773
|
-
let connectionPromise!: Promise<SessionWebSocketCacheEntry>;
|
|
1774
|
-
connectionPromise = connectWebSocket(url, headers, signal, connectTimeoutMs)
|
|
1775
|
-
.then((socket) => {
|
|
1776
|
-
if (websocketConnectionPromises.get(cacheKey) !== connectionPromise) {
|
|
1777
|
-
closeWebSocketSilently(socket, 1000, "session_shutdown");
|
|
1778
|
-
throw new Error("WebSocket session closed");
|
|
1779
|
-
}
|
|
1780
|
-
const entry: SessionWebSocketCacheEntry = { socket, busy: false, waiters: [] };
|
|
1781
|
-
websocketSessionCache.set(cacheKey, entry);
|
|
1782
|
-
return entry;
|
|
1783
|
-
})
|
|
1784
|
-
.finally(() => {
|
|
1785
|
-
if (websocketConnectionPromises.get(cacheKey) === connectionPromise) {
|
|
1786
|
-
websocketConnectionPromises.delete(cacheKey);
|
|
1787
|
-
}
|
|
1788
|
-
});
|
|
1789
|
-
websocketConnectionPromises.set(cacheKey, connectionPromise);
|
|
1790
|
-
pendingConnection = connectionPromise;
|
|
1791
|
-
}
|
|
1792
|
-
const entry = await pendingConnection;
|
|
1793
|
-
if (entry.busy) return waitForCachedWebSocket(cacheKey, entry, signal);
|
|
1794
|
-
return acquireCachedWebSocketEntry(cacheKey, entry, false);
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
function requestBodyWithoutInput(body: ResponsesBody): ResponsesBody {
|
|
1798
|
-
const {
|
|
1799
|
-
input: _input,
|
|
1800
|
-
previous_response_id: _previousResponseId,
|
|
1801
|
-
client_metadata: _clientMetadata,
|
|
1802
|
-
stream_options: _streamOptions,
|
|
1803
|
-
generate: _generate,
|
|
1804
|
-
...rest
|
|
1805
|
-
} = body;
|
|
1806
|
-
return rest as ResponsesBody;
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
function normalizeResponseItemForComparison(value: unknown): unknown {
|
|
1810
|
-
if (Array.isArray(value)) return value.map(normalizeResponseItemForComparison);
|
|
1811
|
-
if (!value || typeof value !== "object") return value;
|
|
1812
|
-
const result: Record<string, unknown> = {};
|
|
1813
|
-
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
1814
|
-
if (key === "internal_chat_message_metadata_passthrough") continue;
|
|
1815
|
-
result[key] = normalizeResponseItemForComparison(entry);
|
|
1816
|
-
}
|
|
1817
|
-
return result;
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
function stableJson(value: unknown): string {
|
|
1821
|
-
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
1822
|
-
if (value && typeof value === "object") {
|
|
1823
|
-
return `{${Object.entries(value as Record<string, unknown>)
|
|
1824
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
1825
|
-
.map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
|
|
1826
|
-
.join(",")}}`;
|
|
1827
|
-
}
|
|
1828
|
-
return JSON.stringify(value) ?? "undefined";
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
function responseInputsEqual(a: unknown[] | undefined, b: unknown[] | undefined): boolean {
|
|
1832
|
-
const left = a ?? [];
|
|
1833
|
-
const right = b ?? [];
|
|
1834
|
-
if (left.length !== right.length) return false;
|
|
1835
|
-
return left.every((item, index) =>
|
|
1836
|
-
stableJson(normalizeResponseItemForComparison(item))
|
|
1837
|
-
=== stableJson(normalizeResponseItemForComparison(right[index])));
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
function requestBodiesMatchExceptInput(a: ResponsesBody, b: ResponsesBody): boolean {
|
|
1841
|
-
return stableJson(requestBodyWithoutInput(a)) === stableJson(requestBodyWithoutInput(b));
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
function getCachedWebSocketInputDelta(body: ResponsesBody, continuation: CachedWebSocketContinuationState): unknown[] | undefined {
|
|
1845
|
-
if (!requestBodiesMatchExceptInput(body, continuation.lastRequestBody)) {
|
|
1846
|
-
return undefined;
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
const currentInput = body.input ?? [];
|
|
1850
|
-
const baseline = [...(continuation.lastRequestBody.input ?? []), ...continuation.lastResponseItems];
|
|
1851
|
-
if (currentInput.length < baseline.length) {
|
|
1852
|
-
return undefined;
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
const prefix = currentInput.slice(0, baseline.length);
|
|
1856
|
-
if (!responseInputsEqual(prefix, baseline)) {
|
|
1857
|
-
return undefined;
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
return currentInput.slice(baseline.length);
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
function buildCachedWebSocketRequestBody(
|
|
1864
|
-
entry: SessionWebSocketCacheEntry,
|
|
1865
|
-
body: ResponsesBody,
|
|
1866
|
-
): ResponsesBody {
|
|
1867
|
-
const continuation = entry.continuation;
|
|
1868
|
-
if (!continuation) {
|
|
1869
|
-
return body;
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
const delta = getCachedWebSocketInputDelta(body, continuation);
|
|
1873
|
-
if (delta === undefined || !continuation.lastResponseId) {
|
|
1874
|
-
entry.continuation = undefined;
|
|
1875
|
-
return body;
|
|
1876
|
-
}
|
|
1877
|
-
return {
|
|
1878
|
-
...body,
|
|
1879
|
-
previous_response_id: continuation.lastResponseId,
|
|
1880
|
-
input: delta,
|
|
1881
|
-
};
|
|
1882
|
-
}
|
|
1883
|
-
|
|
1884
|
-
function isPrefixedResponseItemId(value: string): boolean {
|
|
1885
|
-
const separator = value.indexOf("_");
|
|
1886
|
-
return separator > 0 && separator < value.length - 1;
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
|
-
function prepareResponseItemsForWire(items: unknown[]): unknown[] {
|
|
1890
|
-
return items.map((item) => {
|
|
1891
|
-
if (!item || typeof item !== "object" || Array.isArray(item)) return item;
|
|
1892
|
-
const record = item as Record<string, unknown>;
|
|
1893
|
-
if (typeof record.id !== "string" || isPrefixedResponseItemId(record.id)) return item;
|
|
1894
|
-
const { id: _id, ...rest } = record;
|
|
1895
|
-
return rest;
|
|
1896
|
-
});
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
function prepareWebSocketRequestBodyForWire(body: ResponsesBody): ResponsesBody {
|
|
1900
|
-
return {
|
|
1901
|
-
...body,
|
|
1902
|
-
input: prepareResponseItemsForWire(body.input ?? []),
|
|
1903
|
-
};
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
export async function sendWebSocketRequest(
|
|
1907
|
-
socket: WebSocketLike,
|
|
1908
|
-
payload: string,
|
|
1909
|
-
signal: AbortSignal | undefined,
|
|
1910
|
-
timeoutMs = WEBSOCKET_SEND_TIMEOUT_MS,
|
|
1911
|
-
): Promise<void> {
|
|
1912
|
-
if (signal?.aborted) throw new Error("Request was aborted");
|
|
1913
|
-
await new Promise<void>((resolve, reject) => {
|
|
1914
|
-
let settled = false;
|
|
1915
|
-
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
1916
|
-
const finish = (error?: Error) => {
|
|
1917
|
-
if (settled) return;
|
|
1918
|
-
settled = true;
|
|
1919
|
-
if (timeout) clearTimeout(timeout);
|
|
1920
|
-
signal?.removeEventListener("abort", onAbort);
|
|
1921
|
-
if (error) reject(error);
|
|
1922
|
-
else resolve();
|
|
1923
|
-
};
|
|
1924
|
-
const onAbort = () => finish(new Error("Request was aborted"));
|
|
1925
|
-
timeout = setTimeout(
|
|
1926
|
-
() => finish(new Error(`OpenAI Responses WebSocket send timed out after ${timeoutMs}ms`)),
|
|
1927
|
-
Math.max(1, timeoutMs),
|
|
1928
|
-
);
|
|
1929
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1930
|
-
try {
|
|
1931
|
-
socket.send(payload, (error?: Error) => {
|
|
1932
|
-
if (error) {
|
|
1933
|
-
finish(new Error(`Failed to send OpenAI Responses WebSocket request: ${error.message}`));
|
|
1934
|
-
return;
|
|
1935
|
-
}
|
|
1936
|
-
finish();
|
|
1937
|
-
});
|
|
1938
|
-
} catch (error) {
|
|
1939
|
-
finish(error instanceof Error ? error : new Error(String(error)));
|
|
1940
|
-
}
|
|
1941
|
-
});
|
|
1942
|
-
}
|
|
1943
|
-
|
|
1944
|
-
async function* parseWebSocket(socket: WebSocketLike, signal: AbortSignal | undefined): AsyncIterable<StreamEventShape> {
|
|
1945
|
-
const queue: StreamEventShape[] = [];
|
|
1946
|
-
let pending: (() => void) | null = null;
|
|
1947
|
-
let done = false;
|
|
1948
|
-
let failed: Error | null = null;
|
|
1949
|
-
let closeError: Error | null = null;
|
|
1950
|
-
let sawCompletion = false;
|
|
1951
|
-
let pendingMessages = 0;
|
|
1952
|
-
let messageChain = Promise.resolve();
|
|
1953
|
-
|
|
1954
|
-
const wake = () => {
|
|
1955
|
-
if (!pending) return;
|
|
1956
|
-
const resolve = pending;
|
|
1957
|
-
pending = null;
|
|
1958
|
-
resolve();
|
|
1959
|
-
};
|
|
1960
|
-
|
|
1961
|
-
const onMessage = (event: unknown) => {
|
|
1962
|
-
if (done) return;
|
|
1963
|
-
if (queue.length + pendingMessages >= WEBSOCKET_EVENT_QUEUE_CAPACITY) {
|
|
1964
|
-
failed = new ProviderProtocolError(
|
|
1965
|
-
`OpenAI Responses WebSocket event queue exceeded ${WEBSOCKET_EVENT_QUEUE_CAPACITY} items`,
|
|
1966
|
-
);
|
|
1967
|
-
done = true;
|
|
1968
|
-
wake();
|
|
1969
|
-
return;
|
|
1970
|
-
}
|
|
1971
|
-
pendingMessages++;
|
|
1972
|
-
messageChain = messageChain
|
|
1973
|
-
.then(async () => {
|
|
1974
|
-
if (!event || typeof event !== "object" || !("data" in event)) return;
|
|
1975
|
-
if ((event as { isBinary?: unknown }).isBinary === true) {
|
|
1976
|
-
failed = new ProviderProtocolError("Unexpected binary OpenAI Responses WebSocket event");
|
|
1977
|
-
done = true;
|
|
1978
|
-
return;
|
|
1979
|
-
}
|
|
1980
|
-
const data = (event as { data?: unknown }).data;
|
|
1981
|
-
const text = typeof data === "string"
|
|
1982
|
-
? data
|
|
1983
|
-
: Buffer.isBuffer(data)
|
|
1984
|
-
? data.toString("utf8")
|
|
1985
|
-
: ArrayBuffer.isView(data)
|
|
1986
|
-
? Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8")
|
|
1987
|
-
: null;
|
|
1988
|
-
if (text === null) {
|
|
1989
|
-
failed = new ProviderProtocolError("Unsupported OpenAI Responses WebSocket message payload");
|
|
1990
|
-
done = true;
|
|
1991
|
-
return;
|
|
1992
|
-
}
|
|
1993
|
-
try {
|
|
1994
|
-
const parsed = JSON.parse(text) as StreamEventShape;
|
|
1995
|
-
const type = typeof parsed.type === "string" ? parsed.type : "";
|
|
1996
|
-
if (type === "response.completed" || type === "response.done" || type === "response.incomplete") {
|
|
1997
|
-
sawCompletion = true;
|
|
1998
|
-
closeError = null;
|
|
1999
|
-
done = true;
|
|
2000
|
-
}
|
|
2001
|
-
if (queue.length >= WEBSOCKET_EVENT_QUEUE_CAPACITY) {
|
|
2002
|
-
failed = new ProviderProtocolError(
|
|
2003
|
-
`OpenAI Responses WebSocket event queue exceeded ${WEBSOCKET_EVENT_QUEUE_CAPACITY} items`,
|
|
2004
|
-
);
|
|
2005
|
-
done = true;
|
|
2006
|
-
return;
|
|
2007
|
-
}
|
|
2008
|
-
queue.push(parsed);
|
|
2009
|
-
} catch {
|
|
2010
|
-
// Match Codex: malformed text frames are logged/ignored rather than
|
|
2011
|
-
// tearing down an otherwise healthy response stream.
|
|
2012
|
-
}
|
|
2013
|
-
})
|
|
2014
|
-
.catch((error: unknown) => {
|
|
2015
|
-
failed = error instanceof Error ? error : new Error(String(error));
|
|
2016
|
-
done = true;
|
|
2017
|
-
})
|
|
2018
|
-
.finally(() => {
|
|
2019
|
-
pendingMessages--;
|
|
2020
|
-
wake();
|
|
2021
|
-
});
|
|
2022
|
-
};
|
|
2023
|
-
|
|
2024
|
-
const onError = (event: unknown) => {
|
|
2025
|
-
failed = extractWebSocketError(event);
|
|
2026
|
-
done = true;
|
|
2027
|
-
wake();
|
|
2028
|
-
};
|
|
2029
|
-
|
|
2030
|
-
const onClose = (event: unknown) => {
|
|
2031
|
-
if (sawCompletion) {
|
|
2032
|
-
done = true;
|
|
2033
|
-
wake();
|
|
2034
|
-
return;
|
|
2035
|
-
}
|
|
2036
|
-
if (!closeError) {
|
|
2037
|
-
closeError = extractWebSocketCloseError(event);
|
|
2038
|
-
}
|
|
2039
|
-
done = true;
|
|
2040
|
-
wake();
|
|
2041
|
-
};
|
|
2042
|
-
|
|
2043
|
-
const onAbort = () => {
|
|
2044
|
-
failed = new Error("Request was aborted");
|
|
2045
|
-
done = true;
|
|
2046
|
-
wake();
|
|
2047
|
-
};
|
|
2048
|
-
|
|
2049
|
-
socket.addEventListener("message", onMessage);
|
|
2050
|
-
socket.addEventListener("error", onError);
|
|
2051
|
-
socket.addEventListener("close", onClose);
|
|
2052
|
-
signal?.addEventListener("abort", onAbort);
|
|
2053
|
-
|
|
2054
|
-
try {
|
|
2055
|
-
while (true) {
|
|
2056
|
-
if (signal?.aborted) {
|
|
2057
|
-
throw new Error("Request was aborted");
|
|
2058
|
-
}
|
|
2059
|
-
if (queue.length > 0) {
|
|
2060
|
-
yield queue.shift() as StreamEventShape;
|
|
2061
|
-
continue;
|
|
2062
|
-
}
|
|
2063
|
-
if (done && pendingMessages === 0) break;
|
|
2064
|
-
await new Promise<void>((resolve, reject) => {
|
|
2065
|
-
const timeout = setTimeout(() => {
|
|
2066
|
-
pending = null;
|
|
2067
|
-
reject(new Error(`OpenAI Responses WebSocket idle timeout after ${WEBSOCKET_IDLE_TIMEOUT_MS}ms`));
|
|
2068
|
-
}, WEBSOCKET_IDLE_TIMEOUT_MS);
|
|
2069
|
-
pending = () => {
|
|
2070
|
-
clearTimeout(timeout);
|
|
2071
|
-
resolve();
|
|
2072
|
-
};
|
|
2073
|
-
});
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
if (failed) throw failed;
|
|
2077
|
-
if (closeError && !sawCompletion) throw closeError;
|
|
2078
|
-
if (!sawCompletion) {
|
|
2079
|
-
throw new Error("WebSocket stream closed before response.completed");
|
|
2080
|
-
}
|
|
2081
|
-
} finally {
|
|
2082
|
-
socket.removeEventListener("message", onMessage);
|
|
2083
|
-
socket.removeEventListener("error", onError);
|
|
2084
|
-
socket.removeEventListener("close", onClose);
|
|
2085
|
-
signal?.removeEventListener("abort", onAbort);
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
|
-
async function* startWebSocketOutputOnFirstEvent(
|
|
2090
|
-
events: AsyncIterable<StreamEventShape>,
|
|
2091
|
-
onStart: () => void,
|
|
2092
|
-
): AsyncIterable<StreamEventShape> {
|
|
2093
|
-
let started = false;
|
|
2094
|
-
for await (const event of events) {
|
|
2095
|
-
if (!started && event.type !== "error" && event.type !== "response.failed") {
|
|
2096
|
-
started = true;
|
|
2097
|
-
onStart();
|
|
2098
|
-
}
|
|
2099
|
-
yield event;
|
|
2100
|
-
}
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
async function* countWebSocketEvents(
|
|
2104
|
-
events: AsyncIterable<StreamEventShape>,
|
|
2105
|
-
onEvent: () => void,
|
|
2106
|
-
): AsyncIterable<StreamEventShape> {
|
|
2107
|
-
for await (const event of events) {
|
|
2108
|
-
onEvent();
|
|
2109
|
-
yield event;
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
|
-
function isRetryableEarlyWebSocketError(error: unknown): boolean {
|
|
2114
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2115
|
-
return /^WebSocket (error|closed)(?:\s|$)/.test(message);
|
|
2116
|
-
}
|
|
2117
|
-
|
|
2118
|
-
function retryAfterMsFromHeaders(headers: Record<string, string> | undefined): number | undefined {
|
|
2119
|
-
if (!headers) return undefined;
|
|
2120
|
-
const retryAfterMs = Object.entries(headers).find(([name]) => name.toLowerCase() === "retry-after-ms")?.[1];
|
|
2121
|
-
if (retryAfterMs) {
|
|
2122
|
-
const parsed = Number.parseFloat(retryAfterMs);
|
|
2123
|
-
if (Number.isFinite(parsed) && parsed >= 0) return parsed;
|
|
2124
|
-
}
|
|
2125
|
-
const retryAfter = Object.entries(headers).find(([name]) => name.toLowerCase() === "retry-after")?.[1];
|
|
2126
|
-
if (!retryAfter) return undefined;
|
|
2127
|
-
const seconds = Number.parseFloat(retryAfter);
|
|
2128
|
-
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
|
|
2129
|
-
const date = Date.parse(retryAfter);
|
|
2130
|
-
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : undefined;
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
|
-
function isRetryableWebSocketError(error: unknown): boolean {
|
|
2134
|
-
if (error instanceof WebSocketHandshakeError) {
|
|
2135
|
-
return isRetryableError(error.status, error.body ?? error.message);
|
|
2136
|
-
}
|
|
2137
|
-
if (error instanceof ProviderResponseError) {
|
|
2138
|
-
if (
|
|
2139
|
-
/usage_limit_reached|usage_not_included/i.test(`${error.code ?? ""} ${error.errorType ?? ""}`)
|
|
2140
|
-
) {
|
|
2141
|
-
return false;
|
|
2142
|
-
}
|
|
2143
|
-
if (typeof error.status === "number" && isRetryableError(error.status, error.message)) return true;
|
|
2144
|
-
return /retry|rate.?limit|overloaded|service.?unavailable|connection.?limit/i.test(
|
|
2145
|
-
`${error.code ?? ""} ${error.errorType ?? ""} ${error.message}`,
|
|
2146
|
-
);
|
|
2147
|
-
}
|
|
2148
|
-
if (error instanceof ProviderProtocolError || error instanceof NonRetryableProviderError) return false;
|
|
2149
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2150
|
-
return /websocket|network|connection|socket|timed? out|timeout|fetch failed|terminated|closed before response\.completed|stream closed before response\.completed/i.test(
|
|
2151
|
-
message,
|
|
2152
|
-
);
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
function explicitWebSocketRetryDelayMs(error: unknown): number | undefined {
|
|
2156
|
-
return error instanceof WebSocketHandshakeError
|
|
2157
|
-
? retryAfterMsFromHeaders(error.headers)
|
|
2158
|
-
: error instanceof ProviderResponseError
|
|
2159
|
-
? error.retryAfterMs
|
|
2160
|
-
: undefined;
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
function boundedWebSocketRetryDelayMs(
|
|
2164
|
-
delayMs: number,
|
|
2165
|
-
options: SimpleStreamOptions | undefined,
|
|
2166
|
-
): number {
|
|
2167
|
-
const configuredMax = options?.maxRetryDelayMs;
|
|
2168
|
-
const maxDelay = typeof configuredMax === "number" && Number.isFinite(configuredMax) && configuredMax >= 0
|
|
2169
|
-
? configuredMax
|
|
2170
|
-
: WEBSOCKET_RETRY_MAX_DELAY_MS;
|
|
2171
|
-
if (maxDelay > 0 && delayMs > maxDelay) {
|
|
2172
|
-
throw new NonRetryableProviderError(
|
|
2173
|
-
`WebSocket retry delay ${Math.round(delayMs)}ms exceeds maxRetryDelayMs ${Math.round(maxDelay)}ms`,
|
|
2174
|
-
);
|
|
2175
|
-
}
|
|
2176
|
-
return delayMs;
|
|
2177
|
-
}
|
|
2178
|
-
|
|
2179
|
-
function webSocketRetryDelayMs(
|
|
2180
|
-
error: unknown,
|
|
2181
|
-
retryCount: number,
|
|
2182
|
-
options: SimpleStreamOptions | undefined,
|
|
2183
|
-
): number {
|
|
2184
|
-
const explicit = explicitWebSocketRetryDelayMs(error);
|
|
2185
|
-
const connectionFailure = !(
|
|
2186
|
-
error instanceof WebSocketHandshakeError
|
|
2187
|
-
|| error instanceof ProviderResponseError
|
|
2188
|
-
|| error instanceof ProviderProtocolError
|
|
2189
|
-
);
|
|
2190
|
-
const base = explicit
|
|
2191
|
-
?? (connectionFailure
|
|
2192
|
-
? Math.min(WEBSOCKET_RETRY_MAX_DELAY_MS, 5_000 * 2 ** Math.max(0, retryCount - 1))
|
|
2193
|
-
: WEBSOCKET_RETRY_BASE_DELAY_MS * 2 ** Math.max(0, retryCount - 1));
|
|
2194
|
-
const jittered = explicit === undefined && !connectionFailure
|
|
2195
|
-
? Math.round(base * (0.9 + Math.random() * 0.2))
|
|
2196
|
-
: base;
|
|
2197
|
-
return boundedWebSocketRetryDelayMs(jittered, options);
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
function webSocketCompactionRetryDelayMs(
|
|
2201
|
-
error: unknown,
|
|
2202
|
-
retryCount: number,
|
|
2203
|
-
options: SimpleStreamOptions | undefined,
|
|
2204
|
-
): number {
|
|
2205
|
-
const explicit = explicitWebSocketRetryDelayMs(error);
|
|
2206
|
-
const base = explicit ?? WEBSOCKET_RETRY_BASE_DELAY_MS * 2 ** Math.max(0, retryCount - 1);
|
|
2207
|
-
const jittered = explicit === undefined
|
|
2208
|
-
? Math.round(base * (0.9 + Math.random() * 0.2))
|
|
2209
|
-
: base;
|
|
2210
|
-
return boundedWebSocketRetryDelayMs(jittered, options);
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
function webSocketStreamMaxRetries(options: SimpleStreamOptions | undefined): number {
|
|
2214
|
-
const value = options?.maxRetries;
|
|
2215
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
2216
|
-
return DEFAULT_WEBSOCKET_STREAM_MAX_RETRIES;
|
|
2217
|
-
}
|
|
2218
|
-
return Math.min(MAX_WEBSOCKET_STREAM_MAX_RETRIES, Math.floor(value));
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
|
-
function isProviderNonTransportError(error: unknown): error is ProviderResponseError | ProviderProtocolError {
|
|
2222
|
-
return error instanceof ProviderResponseError || error instanceof ProviderProtocolError;
|
|
2223
|
-
}
|
|
2224
|
-
|
|
2225
|
-
function isWebSocketUpgradeRejectedError(error: unknown): error is WebSocketHandshakeError {
|
|
2226
|
-
// `auto` is capability negotiation, not a generic recovery path. HTTP 426
|
|
2227
|
-
// explicitly tells the client that the WebSocket upgrade cannot be used.
|
|
2228
|
-
// Model/request failures and transient connection errors must stay on the
|
|
2229
|
-
// WebSocket path so an agent-level retry does not silently change transport.
|
|
2230
|
-
return error instanceof WebSocketHandshakeError && error.status === 426;
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
function isWebSocketConnectionLimitReachedError(error: unknown): boolean {
|
|
2234
|
-
const candidate = error as { code?: unknown; message?: unknown };
|
|
2235
|
-
if (candidate?.code === WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE) return true;
|
|
2236
|
-
return typeof candidate?.message === "string" && candidate.message.includes(WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE);
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
function isPreviousResponseNotFoundError(error: unknown): boolean {
|
|
2240
|
-
const candidate = error as { code?: unknown; message?: unknown };
|
|
2241
|
-
if (candidate?.code === PREVIOUS_RESPONSE_NOT_FOUND_CODE) return true;
|
|
2242
|
-
return typeof candidate?.message === "string" && candidate.message.includes(PREVIOUS_RESPONSE_NOT_FOUND_CODE);
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
|
-
function webSocketHeaderIdentity(headers: Headers): string {
|
|
2246
|
-
const requestScoped = new Set([
|
|
2247
|
-
"x-codex-turn-metadata",
|
|
2248
|
-
"x-codex-turn-state",
|
|
2249
|
-
"x-codex-window-id",
|
|
2250
|
-
]);
|
|
2251
|
-
return shortHash(
|
|
2252
|
-
[...headers.entries()]
|
|
2253
|
-
.filter(([name]) => !requestScoped.has(name.toLowerCase()))
|
|
2254
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
2255
|
-
.map(([name, value]) => `${name}:${value}`)
|
|
2256
|
-
.join("\n"),
|
|
2257
|
-
);
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
function webSocketCacheKey(
|
|
2261
|
-
sessionId: string | undefined,
|
|
2262
|
-
model: Model<Api>,
|
|
2263
|
-
url: string,
|
|
2264
|
-
headers: Headers,
|
|
2265
|
-
profileHash?: string,
|
|
2266
|
-
): string | undefined {
|
|
2267
|
-
return sessionId
|
|
2268
|
-
? `${sessionId}\n${model.provider}\n${model.api}\n${model.id}\n${url}\n${profileHash ?? "no-profile"}\n${webSocketHeaderIdentity(headers)}`
|
|
2269
|
-
: undefined;
|
|
2270
|
-
}
|
|
2271
|
-
|
|
2272
|
-
function webSocketFallbackKey(
|
|
2273
|
-
sessionId: string | undefined,
|
|
2274
|
-
model: Model<Api>,
|
|
2275
|
-
url: string,
|
|
2276
|
-
profileHash?: string,
|
|
2277
|
-
): string | undefined {
|
|
2278
|
-
return sessionId
|
|
2279
|
-
? `${sessionId}\n${model.provider}\n${model.api}\n${model.id}\n${url}\n${profileHash ?? "no-profile"}`
|
|
2280
|
-
: undefined;
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
function friendlyUsageLimitMessage(error: StreamEventShape["error"], status: number | undefined): string | undefined {
|
|
2284
|
-
const code = error?.code ?? error?.type ?? "";
|
|
2285
|
-
if (!/usage_limit_reached|usage_not_included/i.test(code)) {
|
|
2286
|
-
return undefined;
|
|
2287
|
-
}
|
|
2288
|
-
const plan = error?.plan_type ? ` (${error.plan_type.toLowerCase()} plan)` : "";
|
|
2289
|
-
const mins = error?.resets_at
|
|
2290
|
-
? Math.max(0, Math.round((error.resets_at * 1000 - Date.now()) / 60_000))
|
|
2291
|
-
: undefined;
|
|
2292
|
-
const when = mins !== undefined ? ` Try again in ~${mins} min.` : "";
|
|
2293
|
-
return `You have hit your OpenAI usage limit${plan}.${when}`.trim();
|
|
2294
|
-
}
|
|
2295
|
-
|
|
2296
|
-
async function prewarmWebSocket(request: WebSocketPrewarmRequest): Promise<void> {
|
|
2297
|
-
if ((request.body.input?.length ?? 0) > 0) {
|
|
2298
|
-
throw new Error("Startup WebSocket prewarm must not include conversation input");
|
|
2299
|
-
}
|
|
2300
|
-
const acquired = await acquireWebSocket(
|
|
2301
|
-
request.url,
|
|
2302
|
-
request.headers,
|
|
2303
|
-
request.cacheKey,
|
|
2304
|
-
request.requestMetadata.sessionId,
|
|
2305
|
-
request.signal,
|
|
2306
|
-
request.connectTimeoutMs ?? WEBSOCKET_CONNECT_TIMEOUT_MS,
|
|
2307
|
-
);
|
|
2308
|
-
const { socket, entry } = acquired;
|
|
2309
|
-
let keepConnection = true;
|
|
2310
|
-
let released = false;
|
|
2311
|
-
const releaseOnce = (options?: { keep?: boolean }) => {
|
|
2312
|
-
if (released) return;
|
|
2313
|
-
released = true;
|
|
2314
|
-
acquired.release(options);
|
|
2315
|
-
};
|
|
2316
|
-
|
|
2317
|
-
try {
|
|
2318
|
-
const fullBody = withWebSocketRequestMetadata(request.body, request.requestMetadata);
|
|
2319
|
-
const prewarmBody: ResponsesBody = {
|
|
2320
|
-
...fullBody,
|
|
2321
|
-
generate: false,
|
|
2322
|
-
};
|
|
2323
|
-
// A startup prewarm is the root of a new session continuation. Never
|
|
2324
|
-
// chain another generate:false request from an existing response.
|
|
2325
|
-
if (entry?.continuation) entry.continuation = undefined;
|
|
2326
|
-
const requestBody = prewarmBody;
|
|
2327
|
-
const wireBody = prepareWebSocketRequestBodyForWire(requestBody);
|
|
2328
|
-
if (request.signal?.aborted) throw new Error("Request was aborted");
|
|
2329
|
-
await sendWebSocketRequest(
|
|
2330
|
-
socket,
|
|
2331
|
-
JSON.stringify({ type: "response.create", ...wireBody }),
|
|
2332
|
-
request.signal,
|
|
2333
|
-
WEBSOCKET_SEND_TIMEOUT_MS,
|
|
2334
|
-
);
|
|
2335
|
-
const responseItems: unknown[] = [];
|
|
2336
|
-
let responseId: string | undefined;
|
|
2337
|
-
for await (const event of mapCodexEvents(parseWebSocket(socket, request.signal), request.requestMetadata.sessionId)) {
|
|
2338
|
-
if (event.type === "response.created" && event.response?.id) responseId = event.response.id;
|
|
2339
|
-
if (event.type === "response.output_item.done" && event.item) responseItems.push(event.item);
|
|
2340
|
-
if (
|
|
2341
|
-
(event.type === "response.completed" || event.type === "response.incomplete")
|
|
2342
|
-
&& event.response?.id
|
|
2343
|
-
) {
|
|
2344
|
-
responseId = event.response.id;
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
if (entry && responseId) {
|
|
2348
|
-
entry.continuation = {
|
|
2349
|
-
lastRequestBody: prewarmBody,
|
|
2350
|
-
lastResponseId: responseId,
|
|
2351
|
-
lastResponseItems: responseItems,
|
|
2352
|
-
};
|
|
2353
|
-
}
|
|
2354
|
-
releaseOnce({ keep: true });
|
|
2355
|
-
} catch (error) {
|
|
2356
|
-
keepConnection = false;
|
|
2357
|
-
if (entry) entry.continuation = undefined;
|
|
2358
|
-
releaseOnce({ keep: false });
|
|
2359
|
-
throw error;
|
|
2360
|
-
} finally {
|
|
2361
|
-
releaseOnce({ keep: keepConnection });
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
|
-
async function* mapCodexEvents(
|
|
2366
|
-
events: AsyncIterable<StreamEventShape>,
|
|
2367
|
-
sessionKey?: string,
|
|
2368
|
-
): AsyncIterable<StreamEventShape> {
|
|
2369
|
-
let sawTerminalResponse = false;
|
|
2370
|
-
const completedOutputItems = new Set<string>();
|
|
2371
|
-
const outputItemKey = (item: unknown, index?: number): string | undefined => {
|
|
2372
|
-
if (!item || typeof item !== "object") return undefined;
|
|
2373
|
-
const candidate = item as { id?: unknown; type?: unknown };
|
|
2374
|
-
if (typeof candidate.id === "string" && typeof candidate.type === "string") return `${candidate.type}:${candidate.id}`;
|
|
2375
|
-
return typeof candidate.type === "string" && typeof index === "number" ? `${candidate.type}:index:${index}` : undefined;
|
|
2376
|
-
};
|
|
2377
|
-
for await (const event of events) {
|
|
2378
|
-
const type = typeof event.type === "string" ? event.type : undefined;
|
|
2379
|
-
if (!type) continue;
|
|
2380
|
-
|
|
2381
|
-
if (type === "response.metadata" && sessionKey) {
|
|
2382
|
-
const eventHeaders = event.headers && typeof event.headers === "object" && !Array.isArray(event.headers)
|
|
2383
|
-
? (event.headers as Record<string, unknown>)
|
|
2384
|
-
: undefined;
|
|
2385
|
-
const turnState = eventHeaders?.["x-codex-turn-state"];
|
|
2386
|
-
if (typeof turnState === "string") {
|
|
2387
|
-
captureCodexTurnState(sessionKey, turnState);
|
|
2388
|
-
}
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
|
-
if (type === "error") {
|
|
2392
|
-
const nestedError = event.error;
|
|
2393
|
-
const status = typeof event.status === "number"
|
|
2394
|
-
? event.status
|
|
2395
|
-
: typeof event.status_code === "number"
|
|
2396
|
-
? event.status_code
|
|
2397
|
-
: undefined;
|
|
2398
|
-
const eventHeaders = event.headers && typeof event.headers === "object" && !Array.isArray(event.headers)
|
|
2399
|
-
? Object.fromEntries(
|
|
2400
|
-
Object.entries(event.headers as Record<string, unknown>)
|
|
2401
|
-
.filter((entry): entry is [string, string | number | boolean] =>
|
|
2402
|
-
typeof entry[1] === "string" || typeof entry[1] === "number" || typeof entry[1] === "boolean")
|
|
2403
|
-
.map(([name, value]) => [name, String(value)]),
|
|
2404
|
-
)
|
|
2405
|
-
: undefined;
|
|
2406
|
-
const code = typeof nestedError?.code === "string"
|
|
2407
|
-
? nestedError.code
|
|
2408
|
-
: typeof event.code === "string"
|
|
2409
|
-
? event.code
|
|
2410
|
-
: undefined;
|
|
2411
|
-
const message = typeof nestedError?.message === "string"
|
|
2412
|
-
? nestedError.message
|
|
2413
|
-
: typeof event.message === "string"
|
|
2414
|
-
? event.message
|
|
2415
|
-
: undefined;
|
|
2416
|
-
const displayMessage = friendlyUsageLimitMessage(nestedError, status)
|
|
2417
|
-
?? `Codex error: ${message || code || JSON.stringify(event)}`;
|
|
2418
|
-
const error = new ProviderResponseError(displayMessage) as ProviderResponseError & {
|
|
2419
|
-
sequenceNumber?: number;
|
|
2420
|
-
};
|
|
2421
|
-
if (code) error.code = code;
|
|
2422
|
-
if (typeof nestedError?.type === "string") error.errorType = nestedError.type;
|
|
2423
|
-
if (status !== undefined) error.status = status;
|
|
2424
|
-
error.retryAfterMs = retryAfterMsFromHeaders(eventHeaders);
|
|
2425
|
-
if (typeof event.sequence_number === "number") error.sequenceNumber = event.sequence_number;
|
|
2426
|
-
throw error;
|
|
2427
|
-
}
|
|
2428
|
-
|
|
2429
|
-
if (type === "response.failed") {
|
|
2430
|
-
const responseError = event.response?.error as { code?: unknown; message?: unknown } | undefined;
|
|
2431
|
-
const error = new ProviderResponseError(
|
|
2432
|
-
typeof responseError?.message === "string" ? responseError.message : "OpenAI Responses request failed",
|
|
2433
|
-
);
|
|
2434
|
-
if (typeof responseError?.code === "string") error.code = responseError.code;
|
|
2435
|
-
throw error;
|
|
2436
|
-
}
|
|
2437
|
-
|
|
2438
|
-
if (type === "response.done" || type === "response.completed" || type === "response.incomplete") {
|
|
2439
|
-
sawTerminalResponse = true;
|
|
2440
|
-
const response = event.response;
|
|
2441
|
-
const output = Array.isArray(response?.output) ? response.output : [];
|
|
2442
|
-
for (let outputIndex = 0; outputIndex < output.length; outputIndex++) {
|
|
2443
|
-
const item = output[outputIndex];
|
|
2444
|
-
if (!item || typeof item !== "object") continue;
|
|
2445
|
-
const itemType = (item as { type?: unknown }).type;
|
|
2446
|
-
if (
|
|
2447
|
-
itemType !== "image_generation_call"
|
|
2448
|
-
&& itemType !== "web_search_call"
|
|
2449
|
-
&& itemType !== "compaction"
|
|
2450
|
-
&& itemType !== "context_compaction"
|
|
2451
|
-
) continue;
|
|
2452
|
-
const key = outputItemKey(item, outputIndex);
|
|
2453
|
-
if (key && completedOutputItems.has(key)) continue;
|
|
2454
|
-
if (key) completedOutputItems.add(key);
|
|
2455
|
-
yield { type: "response.output_item.done", output_index: outputIndex, item } as StreamEventShape;
|
|
2456
|
-
}
|
|
2457
|
-
yield {
|
|
2458
|
-
...event,
|
|
2459
|
-
type: "response.completed",
|
|
2460
|
-
response: response ? { ...response, status: normalizeCodexStatus(response.status) } : response,
|
|
2461
|
-
};
|
|
2462
|
-
return;
|
|
2463
|
-
}
|
|
2464
|
-
|
|
2465
|
-
if (type === "response.output_item.done") {
|
|
2466
|
-
const key = outputItemKey(event.item, typeof event.output_index === "number" ? event.output_index : undefined);
|
|
2467
|
-
if (key) completedOutputItems.add(key);
|
|
2468
|
-
}
|
|
2469
|
-
|
|
2470
|
-
yield event;
|
|
2471
|
-
}
|
|
2472
|
-
|
|
2473
|
-
if (!sawTerminalResponse) {
|
|
2474
|
-
throw new Error("Stream closed before response.completed");
|
|
2475
|
-
}
|
|
2476
|
-
}
|
|
2477
|
-
|
|
2478
|
-
function normalizeCodexStatus(status: string | undefined): string | undefined {
|
|
2479
|
-
if (typeof status !== "string") return undefined;
|
|
2480
|
-
return CODEX_RESPONSE_STATUSES.has(status) ? status : undefined;
|
|
2481
|
-
}
|
|
2482
|
-
|
|
2483
|
-
function getLatestUserText(context: Context): string | undefined {
|
|
2484
|
-
for (let i = context.messages.length - 1; i >= 0; i--) {
|
|
2485
|
-
const message = context.messages[i];
|
|
2486
|
-
if (message.role !== "user") continue;
|
|
2487
|
-
if (typeof message.content === "string") {
|
|
2488
|
-
const trimmed = message.content.trim();
|
|
2489
|
-
if (trimmed) return trimmed;
|
|
2490
|
-
continue;
|
|
2491
|
-
}
|
|
2492
|
-
const text = message.content
|
|
2493
|
-
.filter((item) => item.type === "text")
|
|
2494
|
-
.map((item) => item.text)
|
|
2495
|
-
.join("\n")
|
|
2496
|
-
.trim();
|
|
2497
|
-
if (text) return text;
|
|
2498
|
-
}
|
|
2499
|
-
return undefined;
|
|
2500
|
-
}
|
|
2501
|
-
|
|
2502
|
-
async function* captureGeneratedImages(
|
|
2503
|
-
events: AsyncIterable<StreamEventShape>,
|
|
2504
|
-
options: {
|
|
2505
|
-
cwd: string;
|
|
2506
|
-
requestPrompt?: string;
|
|
2507
|
-
onImageSaved: (image: SavedGeneratedImage, imageData: { data: string; mimeType: string }) => void;
|
|
2508
|
-
onWebSearchCaptured?: (search: SurfacedWebSearch) => void;
|
|
2509
|
-
},
|
|
2510
|
-
): AsyncIterable<StreamEventShape> {
|
|
2511
|
-
let responseId: string | undefined;
|
|
2512
|
-
|
|
2513
|
-
for await (const event of events) {
|
|
2514
|
-
if (event.type === "response.created" && event.response?.id) {
|
|
2515
|
-
responseId = event.response.id;
|
|
2516
|
-
}
|
|
2517
|
-
|
|
2518
|
-
if (event.type === "response.output_item.done" && event.item?.type === "image_generation_call") {
|
|
2519
|
-
const callId = typeof event.item.id === "string" ? event.item.id : undefined;
|
|
2520
|
-
const result = typeof event.item.result === "string" ? event.item.result : undefined;
|
|
2521
|
-
if (callId && result) {
|
|
2522
|
-
try {
|
|
2523
|
-
const outputFormat = typeof event.item.output_format === "string" ? event.item.output_format : undefined;
|
|
2524
|
-
const normalizedOutputFormat = normalizeImageOutputFormat(outputFormat);
|
|
2525
|
-
const settings = loadSettings(options.cwd);
|
|
2526
|
-
const imageModel = typeof event.item.model === "string" ? event.item.model : settings.imageModel;
|
|
2527
|
-
const saved = await saveOpenAICodexGeneratedImage(options.cwd, {
|
|
2528
|
-
responseId,
|
|
2529
|
-
callId,
|
|
2530
|
-
result,
|
|
2531
|
-
outputFormat: normalizedOutputFormat,
|
|
2532
|
-
imageModel,
|
|
2533
|
-
revisedPrompt:
|
|
2534
|
-
typeof event.item.revised_prompt === "string" ? event.item.revised_prompt : options.requestPrompt,
|
|
2535
|
-
});
|
|
2536
|
-
options.onImageSaved(saved, {
|
|
2537
|
-
data: result,
|
|
2538
|
-
mimeType: `image/${normalizedOutputFormat}`,
|
|
2539
|
-
});
|
|
2540
|
-
} catch {
|
|
2541
|
-
// Image persistence is best-effort. Do not write raw diagnostics to
|
|
2542
|
-
// stdout/stderr from inside the TUI; terminal output can corrupt active
|
|
2543
|
-
// widgets and boxes.
|
|
2544
|
-
}
|
|
2545
|
-
}
|
|
2546
|
-
}
|
|
2547
|
-
|
|
2548
|
-
if (
|
|
2549
|
-
(event.type === "response.output_item.added" || event.type === "response.output_item.done")
|
|
2550
|
-
&& event.item?.type === "web_search_call"
|
|
2551
|
-
) {
|
|
2552
|
-
const search = extractWebSearch(event.item, { completed: event.type === "response.output_item.done" });
|
|
2553
|
-
if (search) {
|
|
2554
|
-
options.onWebSearchCaptured?.(search);
|
|
2555
|
-
}
|
|
2556
|
-
}
|
|
2557
|
-
|
|
2558
|
-
const webSearchProgress = extractWebSearchProgress(event);
|
|
2559
|
-
if (webSearchProgress) {
|
|
2560
|
-
options.onWebSearchCaptured?.(webSearchProgress);
|
|
2561
|
-
}
|
|
2562
|
-
|
|
2563
|
-
yield event;
|
|
2564
|
-
}
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
async function processCapturedResponsesStream<TApi extends Api>(
|
|
2568
|
-
events: AsyncIterable<StreamEventShape>,
|
|
2569
|
-
output: AssistantMessage,
|
|
2570
|
-
stream: AssistantMessageEventStream,
|
|
2571
|
-
model: Model<TApi>,
|
|
2572
|
-
options: SimpleStreamOptions | undefined,
|
|
2573
|
-
sessionKey: string | undefined,
|
|
2574
|
-
deps: {
|
|
2575
|
-
onImageSaved?: (savedImage: SavedGeneratedImage, imageData: { data: string; mimeType: string }) => void;
|
|
2576
|
-
},
|
|
2577
|
-
cwd: string,
|
|
2578
|
-
requestPrompt: string | undefined,
|
|
2579
|
-
webSearchCitationSources: ReadonlyArray<WebSearchCitationSource>,
|
|
2580
|
-
historicalCitationSources: ReadonlyArray<CitationSource>,
|
|
2581
|
-
): Promise<{ responseId?: string; responseItems: unknown[] }> {
|
|
2582
|
-
type TextBlock = Extract<AssistantMessage["content"][number], { type: "text" }>;
|
|
2583
|
-
const responseItems: unknown[] = [];
|
|
2584
|
-
let responseId: string | undefined;
|
|
2585
|
-
const webSearchStates = new Map<string, { search: SurfacedWebSearch; block: TextBlock; contentIndex: number }>();
|
|
2586
|
-
const updateWebSearchActivity = (search: SurfacedWebSearch) => {
|
|
2587
|
-
const existing = webSearchStates.get(search.callId);
|
|
2588
|
-
const merged = mergeWebSearchActivity(existing?.search, search);
|
|
2589
|
-
const text = buildWebSearchInlineText(merged, cwd);
|
|
2590
|
-
const textSignature = encodeWebSearchActivityTextSignature(merged.callId, merged.responseItem);
|
|
2591
|
-
if (existing) {
|
|
2592
|
-
existing.search = merged;
|
|
2593
|
-
existing.block.text = text;
|
|
2594
|
-
existing.block.textSignature = textSignature;
|
|
2595
|
-
stream.push({ type: "text_delta", contentIndex: existing.contentIndex, delta: "", partial: output });
|
|
2596
|
-
return;
|
|
2597
|
-
}
|
|
2598
|
-
|
|
2599
|
-
const block: TextBlock = {
|
|
2600
|
-
type: "text",
|
|
2601
|
-
text: "",
|
|
2602
|
-
textSignature,
|
|
2603
|
-
};
|
|
2604
|
-
output.content.push(block);
|
|
2605
|
-
const contentIndex = output.content.length - 1;
|
|
2606
|
-
webSearchStates.set(search.callId, { search: merged, block, contentIndex });
|
|
2607
|
-
stream.push({ type: "text_start", contentIndex, partial: output });
|
|
2608
|
-
block.text = text;
|
|
2609
|
-
stream.push({ type: "text_delta", contentIndex, delta: text, partial: output });
|
|
2610
|
-
};
|
|
2611
|
-
const captureContinuation = async function* (
|
|
2612
|
-
input: AsyncIterable<StreamEventShape>,
|
|
2613
|
-
): AsyncIterable<StreamEventShape> {
|
|
2614
|
-
for await (const event of input) {
|
|
2615
|
-
if (event.type === "response.created" && event.response?.id) responseId = event.response.id;
|
|
2616
|
-
if (event.type === "response.output_item.done" && event.item) {
|
|
2617
|
-
responseItems.push(event.item);
|
|
2618
|
-
}
|
|
2619
|
-
if (
|
|
2620
|
-
(event.type === "response.completed" || event.type === "response.incomplete")
|
|
2621
|
-
&& event.response
|
|
2622
|
-
) {
|
|
2623
|
-
if (event.response.id) responseId = event.response.id;
|
|
2624
|
-
}
|
|
2625
|
-
yield event;
|
|
2626
|
-
}
|
|
2627
|
-
};
|
|
2628
|
-
const tappedEvents = captureGeneratedImages(captureContinuation(mapCodexEvents(events, sessionKey)), {
|
|
2629
|
-
cwd,
|
|
2630
|
-
requestPrompt,
|
|
2631
|
-
onImageSaved: (image, imageData) => deps.onImageSaved?.(image, imageData),
|
|
2632
|
-
onWebSearchCaptured: updateWebSearchActivity,
|
|
2633
|
-
});
|
|
2634
|
-
|
|
2635
|
-
await processResponsesStream(tappedEvents as AsyncIterable<never>, output, stream, model, {
|
|
2636
|
-
serviceTier: (options as { serviceTier?: ServiceTier } | undefined)?.serviceTier,
|
|
2637
|
-
resolveServiceTier: resolveCodexServiceTier,
|
|
2638
|
-
applyServiceTierPricing: (usage, serviceTier) =>
|
|
2639
|
-
applyServiceTierPricing(usage, serviceTier, model as Model<Api>, cwd),
|
|
2640
|
-
webSearchCitationSources,
|
|
2641
|
-
historicalCitationSources,
|
|
2642
|
-
});
|
|
2643
|
-
return { responseId: responseId ?? output.responseId, responseItems };
|
|
2644
|
-
}
|
|
2645
|
-
|
|
2646
|
-
function compactUrl(baseUrl: string | undefined, apiKeyMode: boolean): string {
|
|
2647
|
-
return `${resolveCodexUrl(baseUrl, { apiKeyMode }).replace(/\/+$/, "")}/compact`;
|
|
2648
|
-
}
|
|
2649
|
-
|
|
2650
|
-
function buildJsonHeaders(
|
|
2651
|
-
modelHeaders: ProviderHeaders | undefined,
|
|
2652
|
-
additionalHeaders: ProviderHeaders | undefined,
|
|
2653
|
-
accountId: string | undefined,
|
|
2654
|
-
apiKey: string,
|
|
2655
|
-
sessionId?: string,
|
|
2656
|
-
requestIdentity?: CodexRequestIdentity,
|
|
2657
|
-
): Headers {
|
|
2658
|
-
const headers = buildBaseCodexHeaders(modelHeaders, additionalHeaders, accountId, apiKey);
|
|
2659
|
-
setProviderDefaultHeader(headers, "accept", "application/json");
|
|
2660
|
-
setProviderDefaultHeader(headers, "content-type", "application/json");
|
|
2661
|
-
applyWireIdentityHeaders(
|
|
2662
|
-
headers,
|
|
2663
|
-
sessionId,
|
|
2664
|
-
requestIdentity?.threadId ?? sessionId,
|
|
2665
|
-
requestIdentity,
|
|
2666
|
-
);
|
|
2667
|
-
return headers;
|
|
2668
|
-
}
|
|
2669
|
-
|
|
2670
|
-
async function postJsonWithRetries(
|
|
2671
|
-
url: string,
|
|
2672
|
-
headers: Headers,
|
|
2673
|
-
body: unknown,
|
|
2674
|
-
signal: AbortSignal | undefined,
|
|
2675
|
-
timeoutMs = SSE_RESPONSE_HEADER_TIMEOUT_MS,
|
|
2676
|
-
): Promise<Record<string, unknown>> {
|
|
2677
|
-
const bodyJson = JSON.stringify(body);
|
|
2678
|
-
const dispatcher = await proxyDispatcherForUrl(url);
|
|
2679
|
-
let lastError: Error | undefined;
|
|
2680
|
-
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
2681
|
-
try {
|
|
2682
|
-
const response = await fetchWithResponseHeaderTimeout(url, {
|
|
2683
|
-
method: "POST",
|
|
2684
|
-
headers,
|
|
2685
|
-
body: bodyJson,
|
|
2686
|
-
...(dispatcher ? { dispatcher } : {}),
|
|
2687
|
-
} as RequestInit, signal, timeoutMs);
|
|
2688
|
-
if (response.ok) {
|
|
2689
|
-
const parsed = await response.json();
|
|
2690
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2691
|
-
throw new NonRetryableProviderError("OpenAI native compaction returned a non-object response");
|
|
2692
|
-
}
|
|
2693
|
-
return parsed as Record<string, unknown>;
|
|
2694
|
-
}
|
|
2695
|
-
const errorText = await response.text();
|
|
2696
|
-
if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
|
|
2697
|
-
await sleep(BASE_DELAY_MS * 2 ** attempt, signal);
|
|
2698
|
-
continue;
|
|
2699
|
-
}
|
|
2700
|
-
const info = await parseErrorResponse(new Response(errorText, {
|
|
2701
|
-
status: response.status,
|
|
2702
|
-
statusText: response.statusText,
|
|
2703
|
-
}));
|
|
2704
|
-
throw new NonRetryableProviderError(withHttpStatusPrefix(response.status, info.friendlyMessage || info.message));
|
|
2705
|
-
} catch (error) {
|
|
2706
|
-
if (error instanceof NonRetryableProviderError) throw error;
|
|
2707
|
-
if (signal?.aborted) throw new Error("Request was aborted");
|
|
2708
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
2709
|
-
if (attempt < MAX_RETRIES) {
|
|
2710
|
-
await sleep(BASE_DELAY_MS * 2 ** attempt, signal);
|
|
2711
|
-
continue;
|
|
2712
|
-
}
|
|
2713
|
-
throw lastError;
|
|
2714
|
-
}
|
|
2715
|
-
}
|
|
2716
|
-
throw lastError ?? new Error("OpenAI native compaction failed");
|
|
2717
|
-
}
|
|
2718
|
-
|
|
2719
|
-
function compactionItems(output: unknown): unknown[] {
|
|
2720
|
-
if (!Array.isArray(output)) throw new Error("OpenAI native compaction response omitted output");
|
|
2721
|
-
return output.filter(isNativeCompactionItem);
|
|
2722
|
-
}
|
|
2723
|
-
|
|
2724
|
-
function isNativeCompactionItem(item: unknown): item is Record<string, unknown> {
|
|
2725
|
-
const type = item && typeof item === "object" ? (item as { type?: unknown }).type : undefined;
|
|
2726
|
-
return type === "compaction" || type === "context_compaction";
|
|
2727
|
-
}
|
|
2728
|
-
|
|
2729
|
-
function approxTokenCount(text: string): number {
|
|
2730
|
-
const bytes = new TextEncoder().encode(text).byteLength;
|
|
2731
|
-
return Math.ceil(bytes / APPROX_BYTES_PER_TOKEN);
|
|
2732
|
-
}
|
|
2733
|
-
|
|
2734
|
-
function responseItemTokenCount(item: Record<string, unknown>): number {
|
|
2735
|
-
if (item.type === "message" && Array.isArray(item.content)) {
|
|
2736
|
-
const tokens = item.content.reduce((total, part) => {
|
|
2737
|
-
if (!part || typeof part !== "object") return total;
|
|
2738
|
-
const text = (part as { text?: unknown }).text;
|
|
2739
|
-
return typeof text === "string" ? total + approxTokenCount(text) : total;
|
|
2740
|
-
}, 0);
|
|
2741
|
-
return Math.max(1, tokens);
|
|
2742
|
-
}
|
|
2743
|
-
try {
|
|
2744
|
-
return Math.max(1, approxTokenCount(JSON.stringify(item)));
|
|
2745
|
-
} catch {
|
|
2746
|
-
return Number.MAX_SAFE_INTEGER;
|
|
2747
|
-
}
|
|
2748
|
-
}
|
|
2749
|
-
|
|
2750
|
-
function truncateUtf8Prefix(text: string, maxBytes: number): string {
|
|
2751
|
-
if (maxBytes <= 0) return "";
|
|
2752
|
-
const encoder = new TextEncoder();
|
|
2753
|
-
if (encoder.encode(text).byteLength <= maxBytes) return text;
|
|
2754
|
-
let result = "";
|
|
2755
|
-
let bytes = 0;
|
|
2756
|
-
for (const character of text) {
|
|
2757
|
-
const characterBytes = encoder.encode(character).byteLength;
|
|
2758
|
-
if (bytes + characterBytes > maxBytes) break;
|
|
2759
|
-
result += character;
|
|
2760
|
-
bytes += characterBytes;
|
|
2761
|
-
}
|
|
2762
|
-
return result;
|
|
2763
|
-
}
|
|
2764
|
-
|
|
2765
|
-
function truncateResponseMessage(
|
|
2766
|
-
item: Record<string, unknown>,
|
|
2767
|
-
maxTokens: number,
|
|
2768
|
-
): Record<string, unknown> | undefined {
|
|
2769
|
-
if (item.type !== "message" || !Array.isArray(item.content) || maxTokens <= 0) return undefined;
|
|
2770
|
-
let remaining = maxTokens;
|
|
2771
|
-
const content: unknown[] = [];
|
|
2772
|
-
for (const part of item.content) {
|
|
2773
|
-
if (!part || typeof part !== "object" || Array.isArray(part)) continue;
|
|
2774
|
-
const record = part as Record<string, unknown>;
|
|
2775
|
-
if (typeof record.text !== "string") {
|
|
2776
|
-
content.push(part);
|
|
2777
|
-
continue;
|
|
2778
|
-
}
|
|
2779
|
-
if (remaining <= 0) continue;
|
|
2780
|
-
const tokenCount = approxTokenCount(record.text);
|
|
2781
|
-
if (tokenCount <= remaining) {
|
|
2782
|
-
content.push(part);
|
|
2783
|
-
remaining -= tokenCount;
|
|
2784
|
-
continue;
|
|
2785
|
-
}
|
|
2786
|
-
const text = truncateUtf8Prefix(record.text, remaining * APPROX_BYTES_PER_TOKEN);
|
|
2787
|
-
if (text) content.push({ ...record, text });
|
|
2788
|
-
remaining = 0;
|
|
2789
|
-
}
|
|
2790
|
-
return content.length > 0 ? { ...item, content } : undefined;
|
|
2791
|
-
}
|
|
2792
|
-
|
|
2793
|
-
function retainedResponsesCompactionItem(item: unknown): Record<string, unknown> | undefined {
|
|
2794
|
-
if (!item || typeof item !== "object" || Array.isArray(item)) return undefined;
|
|
2795
|
-
const record = item as Record<string, unknown>;
|
|
2796
|
-
if (
|
|
2797
|
-
(record.type === undefined || record.type === "message")
|
|
2798
|
-
&& record.role === "user"
|
|
2799
|
-
&& Array.isArray(record.content)
|
|
2800
|
-
) {
|
|
2801
|
-
return { ...record, type: "message" };
|
|
2802
|
-
}
|
|
2803
|
-
if (record.type !== "agent_message" || !Array.isArray(record.content)) return undefined;
|
|
2804
|
-
const first = record.content[0];
|
|
2805
|
-
const firstText = first && typeof first === "object" ? (first as { text?: unknown }).text : undefined;
|
|
2806
|
-
if (typeof firstText === "string" && firstText.startsWith("Message Type: FINAL_ANSWER\n")) {
|
|
2807
|
-
return undefined;
|
|
2808
|
-
}
|
|
2809
|
-
return responseItemTokenCount(record) <= CODEX_MAX_RETAINED_AGENT_MESSAGE_TOKENS
|
|
2810
|
-
? record
|
|
2811
|
-
: undefined;
|
|
2812
|
-
}
|
|
2813
|
-
|
|
2814
|
-
/**
|
|
2815
|
-
* Match Codex remote compaction v2's installed checkpoint shape: retain the
|
|
2816
|
-
* newest real user messages plus bounded delegated-agent state, drop stale
|
|
2817
|
-
* developer/system/assistant/tool state, then append the opaque compaction
|
|
2818
|
-
* item returned by Responses.
|
|
2819
|
-
*/
|
|
2820
|
-
export function buildCodexCompactionCheckpoint(
|
|
2821
|
-
input: unknown[],
|
|
2822
|
-
compactionItem: unknown,
|
|
2823
|
-
): unknown[] {
|
|
2824
|
-
const candidates = input
|
|
2825
|
-
.map(retainedResponsesCompactionItem)
|
|
2826
|
-
.filter((item): item is Record<string, unknown> => !!item);
|
|
2827
|
-
let remaining = CODEX_RETAINED_MESSAGE_TOKEN_BUDGET;
|
|
2828
|
-
const retainedReversed: Record<string, unknown>[] = [];
|
|
2829
|
-
for (let index = candidates.length - 1; index >= 0 && remaining > 0; index--) {
|
|
2830
|
-
const item = candidates[index]!;
|
|
2831
|
-
const tokenCount = responseItemTokenCount(item);
|
|
2832
|
-
if (tokenCount <= remaining) {
|
|
2833
|
-
retainedReversed.push(item);
|
|
2834
|
-
remaining -= tokenCount;
|
|
2835
|
-
continue;
|
|
2836
|
-
}
|
|
2837
|
-
const truncated = truncateResponseMessage(item, remaining);
|
|
2838
|
-
if (truncated) {
|
|
2839
|
-
retainedReversed.push(truncated);
|
|
2840
|
-
remaining = 0;
|
|
2841
|
-
}
|
|
2842
|
-
}
|
|
2843
|
-
retainedReversed.reverse();
|
|
2844
|
-
return [...retainedReversed, compactionItem];
|
|
2845
|
-
}
|
|
2846
|
-
|
|
2847
|
-
interface CodexCompactionStreamResult {
|
|
2848
|
-
item: unknown;
|
|
2849
|
-
responseId?: string;
|
|
2850
|
-
responseItems: unknown[];
|
|
2851
|
-
}
|
|
2852
|
-
|
|
2853
|
-
async function collectCodexCompactionStream(
|
|
2854
|
-
events: AsyncIterable<StreamEventShape>,
|
|
2855
|
-
): Promise<CodexCompactionStreamResult> {
|
|
2856
|
-
let outputItemCount = 0;
|
|
2857
|
-
const compacted: unknown[] = [];
|
|
2858
|
-
const responseItems: unknown[] = [];
|
|
2859
|
-
let responseId: string | undefined;
|
|
2860
|
-
for await (const event of events) {
|
|
2861
|
-
if (event.type === "response.created" && event.response?.id) {
|
|
2862
|
-
responseId = event.response.id;
|
|
2863
|
-
}
|
|
2864
|
-
if (event.type === "response.output_item.done" && event.item) {
|
|
2865
|
-
outputItemCount++;
|
|
2866
|
-
responseItems.push(event.item);
|
|
2867
|
-
if (isNativeCompactionItem(event.item)) compacted.push(event.item);
|
|
2868
|
-
}
|
|
2869
|
-
if (
|
|
2870
|
-
(event.type === "response.completed" || event.type === "response.incomplete")
|
|
2871
|
-
&& event.response?.id
|
|
2872
|
-
) {
|
|
2873
|
-
responseId = event.response.id;
|
|
2874
|
-
}
|
|
2875
|
-
}
|
|
2876
|
-
if (compacted.length !== 1) {
|
|
2877
|
-
throw new NonRetryableProviderError(
|
|
2878
|
-
`OpenAI compaction trigger expected exactly one compaction item, received ${compacted.length} from ${outputItemCount} output items`,
|
|
2879
|
-
);
|
|
2880
|
-
}
|
|
2881
|
-
return {
|
|
2882
|
-
item: compacted[0],
|
|
2883
|
-
...(responseId ? { responseId } : {}),
|
|
2884
|
-
responseItems,
|
|
2885
|
-
};
|
|
2886
|
-
}
|
|
2887
|
-
|
|
2888
|
-
async function collectCodexCompactionOutput(response: Response, sessionKey?: string): Promise<unknown> {
|
|
2889
|
-
const result = await collectCodexCompactionStream(mapCodexEvents(parseSSE(response), sessionKey));
|
|
2890
|
-
return result.item;
|
|
2891
|
-
}
|
|
2892
|
-
|
|
2893
|
-
async function requestCodexCompactionTrigger(
|
|
2894
|
-
url: string,
|
|
2895
|
-
headers: Headers,
|
|
2896
|
-
body: ResponsesBody,
|
|
2897
|
-
signal: AbortSignal | undefined,
|
|
2898
|
-
sessionKey?: string,
|
|
2899
|
-
): Promise<unknown> {
|
|
2900
|
-
const bodyJson = JSON.stringify(body);
|
|
2901
|
-
const dispatcher = await proxyDispatcherForUrl(url);
|
|
2902
|
-
let lastError: Error | undefined;
|
|
2903
|
-
const maxRetries = Math.min(MAX_RETRIES, CODEX_REMOTE_COMPACTION_STREAM_RETRIES);
|
|
2904
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
2905
|
-
try {
|
|
2906
|
-
const response = await fetchWithResponseHeaderTimeout(url, {
|
|
2907
|
-
method: "POST",
|
|
2908
|
-
headers,
|
|
2909
|
-
body: bodyJson,
|
|
2910
|
-
...(dispatcher ? { dispatcher } : {}),
|
|
2911
|
-
} as RequestInit, signal);
|
|
2912
|
-
if (response.ok) return await collectCodexCompactionOutput(response, sessionKey);
|
|
2913
|
-
|
|
2914
|
-
const errorText = await response.text();
|
|
2915
|
-
if (attempt < maxRetries && isRetryableError(response.status, errorText)) {
|
|
2916
|
-
await sleep(BASE_DELAY_MS * 2 ** attempt, signal);
|
|
2917
|
-
continue;
|
|
2918
|
-
}
|
|
2919
|
-
const info = await parseErrorResponse(new Response(errorText, {
|
|
2920
|
-
status: response.status,
|
|
2921
|
-
statusText: response.statusText,
|
|
2922
|
-
}));
|
|
2923
|
-
throw new NonRetryableProviderError(withHttpStatusPrefix(response.status, info.friendlyMessage || info.message));
|
|
2924
|
-
} catch (error) {
|
|
2925
|
-
if (error instanceof NonRetryableProviderError) throw error;
|
|
2926
|
-
if (signal?.aborted) throw new Error("Request was aborted");
|
|
2927
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
2928
|
-
if (attempt < maxRetries) {
|
|
2929
|
-
await sleep(BASE_DELAY_MS * 2 ** attempt, signal);
|
|
2930
|
-
continue;
|
|
2931
|
-
}
|
|
2932
|
-
throw lastError;
|
|
2933
|
-
}
|
|
2934
|
-
}
|
|
2935
|
-
throw lastError ?? new Error("OpenAI compaction trigger failed");
|
|
2936
|
-
}
|
|
2937
|
-
|
|
2938
|
-
async function requestCodexCompactionTriggerWebSocket(
|
|
2939
|
-
url: string,
|
|
2940
|
-
headers: Headers,
|
|
2941
|
-
body: ResponsesBody,
|
|
2942
|
-
model: Model<Api>,
|
|
2943
|
-
requestMetadata: WebSocketRequestMetadata,
|
|
2944
|
-
signal: AbortSignal | undefined,
|
|
2945
|
-
profileHash?: string,
|
|
2946
|
-
): Promise<unknown> {
|
|
2947
|
-
let disableCachedContext = false;
|
|
2948
|
-
let staleSocketRetried = false;
|
|
2949
|
-
let missingPreviousResponseRetried = false;
|
|
2950
|
-
|
|
2951
|
-
while (true) {
|
|
2952
|
-
const cacheKey = webSocketCacheKey(
|
|
2953
|
-
requestMetadata.sessionId,
|
|
2954
|
-
model,
|
|
2955
|
-
url,
|
|
2956
|
-
headers,
|
|
2957
|
-
profileHash,
|
|
2958
|
-
);
|
|
2959
|
-
const { socket, entry, release, reused } = await acquireWebSocket(
|
|
2960
|
-
url,
|
|
2961
|
-
headers,
|
|
2962
|
-
cacheKey,
|
|
2963
|
-
requestMetadata.sessionId,
|
|
2964
|
-
signal,
|
|
2965
|
-
WEBSOCKET_CONNECT_TIMEOUT_MS,
|
|
2966
|
-
);
|
|
2967
|
-
let keepConnection = true;
|
|
2968
|
-
let released = false;
|
|
2969
|
-
let eventCount = 0;
|
|
2970
|
-
// All reusable WebSocket transports opportunistically continue an exact
|
|
2971
|
-
// logical request prefix. `websocket` still sends a full request whenever
|
|
2972
|
-
// the stable fields or input prefix do not match.
|
|
2973
|
-
const useCachedContext = true;
|
|
2974
|
-
const fullBody = withWebSocketRequestMetadata(body, requestMetadata);
|
|
2975
|
-
const requestBody = useCachedContext && !disableCachedContext && entry
|
|
2976
|
-
? buildCachedWebSocketRequestBody(entry, fullBody)
|
|
2977
|
-
: fullBody;
|
|
2978
|
-
const wireRequestBody = prepareWebSocketRequestBodyForWire(requestBody);
|
|
2979
|
-
const releaseOnce = (releaseOptions?: { keep?: boolean }) => {
|
|
2980
|
-
if (released) return;
|
|
2981
|
-
released = true;
|
|
2982
|
-
release(releaseOptions);
|
|
2983
|
-
};
|
|
2984
|
-
|
|
2985
|
-
try {
|
|
2986
|
-
await sendWebSocketRequest(
|
|
2987
|
-
socket,
|
|
2988
|
-
JSON.stringify({ type: "response.create", ...wireRequestBody }),
|
|
2989
|
-
signal,
|
|
2990
|
-
WEBSOCKET_SEND_TIMEOUT_MS,
|
|
2991
|
-
);
|
|
2992
|
-
const result = await collectCodexCompactionStream(
|
|
2993
|
-
mapCodexEvents(
|
|
2994
|
-
countWebSocketEvents(parseWebSocket(socket, signal), () => {
|
|
2995
|
-
eventCount++;
|
|
2996
|
-
}),
|
|
2997
|
-
requestMetadata.sessionId,
|
|
2998
|
-
),
|
|
2999
|
-
);
|
|
3000
|
-
if (signal?.aborted) {
|
|
3001
|
-
keepConnection = false;
|
|
3002
|
-
throw new Error("Request was aborted");
|
|
3003
|
-
}
|
|
3004
|
-
if (entry && result.responseId) {
|
|
3005
|
-
entry.continuation = {
|
|
3006
|
-
lastRequestBody: fullBody,
|
|
3007
|
-
lastResponseId: result.responseId,
|
|
3008
|
-
lastResponseItems: result.responseItems,
|
|
3009
|
-
};
|
|
3010
|
-
} else if (entry) {
|
|
3011
|
-
entry.continuation = undefined;
|
|
3012
|
-
}
|
|
3013
|
-
releaseOnce({ keep: true });
|
|
3014
|
-
return result.item;
|
|
3015
|
-
} catch (error) {
|
|
3016
|
-
if (entry) entry.continuation = undefined;
|
|
3017
|
-
keepConnection = false;
|
|
3018
|
-
releaseOnce({ keep: false });
|
|
3019
|
-
if (
|
|
3020
|
-
!staleSocketRetried
|
|
3021
|
-
&& reused
|
|
3022
|
-
&& eventCount === 0
|
|
3023
|
-
&& !signal?.aborted
|
|
3024
|
-
&& isRetryableEarlyWebSocketError(error)
|
|
3025
|
-
) {
|
|
3026
|
-
staleSocketRetried = true;
|
|
3027
|
-
continue;
|
|
3028
|
-
}
|
|
3029
|
-
if (
|
|
3030
|
-
!missingPreviousResponseRetried
|
|
3031
|
-
&& requestBody.previous_response_id
|
|
3032
|
-
&& !signal?.aborted
|
|
3033
|
-
&& isPreviousResponseNotFoundError(error)
|
|
3034
|
-
) {
|
|
3035
|
-
missingPreviousResponseRetried = true;
|
|
3036
|
-
disableCachedContext = true;
|
|
3037
|
-
continue;
|
|
3038
|
-
}
|
|
3039
|
-
throw error;
|
|
3040
|
-
} finally {
|
|
3041
|
-
releaseOnce({ keep: keepConnection });
|
|
3042
|
-
}
|
|
3043
|
-
}
|
|
3044
|
-
}
|
|
3045
|
-
|
|
3046
|
-
async function requestCodexCompactionTriggerWithTransport(
|
|
3047
|
-
model: Model<Api>,
|
|
3048
|
-
headers: {
|
|
3049
|
-
sse: Headers;
|
|
3050
|
-
websocket: Headers;
|
|
3051
|
-
},
|
|
3052
|
-
body: ResponsesBody,
|
|
3053
|
-
options: {
|
|
3054
|
-
sessionId?: string;
|
|
3055
|
-
turnId?: string;
|
|
3056
|
-
requestIdentity?: CodexRequestIdentity;
|
|
3057
|
-
signal?: AbortSignal;
|
|
3058
|
-
settings: ResolvedCodexModelSettings;
|
|
3059
|
-
maxRetries?: number;
|
|
3060
|
-
maxRetryDelayMs?: number;
|
|
3061
|
-
},
|
|
3062
|
-
): Promise<unknown> {
|
|
3063
|
-
const transport = options.settings.openaiTransport;
|
|
3064
|
-
const responsesMode = resolveCodexRequestProfile(options.settings.requestProfile).responsesMode;
|
|
3065
|
-
const sseUrl = resolveCodexUrl(model.baseUrl, { apiKeyMode: options.settings.apiKeyMode });
|
|
3066
|
-
const requestMetadata: WebSocketRequestMetadata = {
|
|
3067
|
-
...(options.sessionId ? { sessionId: options.sessionId } : {}),
|
|
3068
|
-
...(options.requestIdentity?.threadId
|
|
3069
|
-
? { threadId: options.requestIdentity.threadId }
|
|
3070
|
-
: {}),
|
|
3071
|
-
turnId: options.requestIdentity?.turnId
|
|
3072
|
-
|| options.turnId
|
|
3073
|
-
|| createPiTurnId(),
|
|
3074
|
-
requestKind: "compaction",
|
|
3075
|
-
...(options.requestIdentity
|
|
3076
|
-
? { identity: options.requestIdentity }
|
|
3077
|
-
: {}),
|
|
3078
|
-
};
|
|
3079
|
-
if (transport === "sse") {
|
|
3080
|
-
return requestCodexCompactionTrigger(
|
|
3081
|
-
sseUrl,
|
|
3082
|
-
headers.sse,
|
|
3083
|
-
withSseRequestMetadata(body, requestMetadata),
|
|
3084
|
-
options.signal,
|
|
3085
|
-
options.sessionId,
|
|
3086
|
-
);
|
|
3087
|
-
}
|
|
3088
|
-
|
|
3089
|
-
const websocketUrl = resolveResponsesWebSocketUrl(model.baseUrl, { apiKeyMode: options.settings.apiKeyMode });
|
|
3090
|
-
const fallbackKey = webSocketFallbackKey(
|
|
3091
|
-
options.sessionId,
|
|
3092
|
-
model,
|
|
3093
|
-
websocketUrl,
|
|
3094
|
-
options.settings.modelProfileHash,
|
|
3095
|
-
);
|
|
3096
|
-
if (
|
|
3097
|
-
transport === "auto"
|
|
3098
|
-
&& fallbackKey
|
|
3099
|
-
&& websocketHttpFallbackSessions.has(fallbackKey)
|
|
3100
|
-
) {
|
|
3101
|
-
return requestCodexCompactionTrigger(
|
|
3102
|
-
sseUrl,
|
|
3103
|
-
headers.sse,
|
|
3104
|
-
withSseRequestMetadata(body, requestMetadata),
|
|
3105
|
-
options.signal,
|
|
3106
|
-
options.sessionId,
|
|
3107
|
-
);
|
|
3108
|
-
}
|
|
3109
|
-
|
|
3110
|
-
const maxRetries = Math.min(
|
|
3111
|
-
CODEX_REMOTE_COMPACTION_STREAM_RETRIES,
|
|
3112
|
-
webSocketStreamMaxRetries({
|
|
3113
|
-
maxRetries: options.maxRetries,
|
|
3114
|
-
} as SimpleStreamOptions),
|
|
3115
|
-
);
|
|
3116
|
-
const retryOptions = {
|
|
3117
|
-
...(options.maxRetryDelayMs !== undefined ? { maxRetryDelayMs: options.maxRetryDelayMs } : {}),
|
|
3118
|
-
} as SimpleStreamOptions;
|
|
3119
|
-
let retries = 0;
|
|
3120
|
-
while (true) {
|
|
3121
|
-
try {
|
|
3122
|
-
return await requestCodexCompactionTriggerWebSocket(
|
|
3123
|
-
websocketUrl,
|
|
3124
|
-
headers.websocket,
|
|
3125
|
-
withResponsesLiteWebSocketMetadata(body, responsesMode),
|
|
3126
|
-
model,
|
|
3127
|
-
requestMetadata,
|
|
3128
|
-
options.signal,
|
|
3129
|
-
options.settings.modelProfileHash,
|
|
3130
|
-
);
|
|
3131
|
-
} catch (error) {
|
|
3132
|
-
if (options.signal?.aborted) throw new Error("Request was aborted");
|
|
3133
|
-
const upgradeRejected = isWebSocketUpgradeRejectedError(error);
|
|
3134
|
-
if (transport === "auto" && upgradeRejected) {
|
|
3135
|
-
if (fallbackKey) websocketHttpFallbackSessions.add(fallbackKey);
|
|
3136
|
-
break;
|
|
3137
|
-
}
|
|
3138
|
-
const retryable = isWebSocketConnectionLimitReachedError(error) || isRetryableWebSocketError(error);
|
|
3139
|
-
if (retryable && retries < maxRetries) {
|
|
3140
|
-
retries++;
|
|
3141
|
-
await sleep(webSocketCompactionRetryDelayMs(error, retries, retryOptions), options.signal);
|
|
3142
|
-
continue;
|
|
3143
|
-
}
|
|
3144
|
-
throw error;
|
|
3145
|
-
}
|
|
3146
|
-
}
|
|
3147
|
-
|
|
3148
|
-
return requestCodexCompactionTrigger(
|
|
3149
|
-
sseUrl,
|
|
3150
|
-
headers.sse,
|
|
3151
|
-
withSseRequestMetadata(body, requestMetadata),
|
|
3152
|
-
options.signal,
|
|
3153
|
-
options.sessionId,
|
|
3154
|
-
);
|
|
3155
|
-
}
|
|
3156
|
-
|
|
3157
|
-
function hasNonEmptyResponseMessageContent(item: Record<string, unknown>): boolean {
|
|
3158
|
-
if (item.type !== "message") return false;
|
|
3159
|
-
if (item.role !== "user" && item.role !== "assistant") return false;
|
|
3160
|
-
if (!Array.isArray(item.content)) return false;
|
|
3161
|
-
return item.content.some((part) => {
|
|
3162
|
-
if (!part || typeof part !== "object") return false;
|
|
3163
|
-
const content = part as Record<string, unknown>;
|
|
3164
|
-
return (
|
|
3165
|
-
(typeof content.text === "string" && content.text.trim().length > 0)
|
|
3166
|
-
|| (typeof content.refusal === "string" && content.refusal.trim().length > 0)
|
|
3167
|
-
|| typeof content.image_url === "string"
|
|
3168
|
-
);
|
|
3169
|
-
});
|
|
3170
|
-
}
|
|
3171
|
-
|
|
3172
|
-
/**
|
|
3173
|
-
* Install remote compaction as a fresh history checkpoint, following Codex's
|
|
3174
|
-
* compaction reducer: preserve only safe message/checkpoint items and discard
|
|
3175
|
-
* reasoning, tool calls, and tool outputs. Replaying a partial call pair is
|
|
3176
|
-
* invalid Responses input and can detach tool arguments from their result.
|
|
3177
|
-
*/
|
|
3178
|
-
export function sanitizeNativeCompactionOutput(output: unknown[]): unknown[] {
|
|
3179
|
-
return output.filter((item) => {
|
|
3180
|
-
if (!item || typeof item !== "object" || Array.isArray(item)) return false;
|
|
3181
|
-
const record = item as Record<string, unknown>;
|
|
3182
|
-
if (record.type === "compaction" || record.type === "context_compaction") return true;
|
|
3183
|
-
return hasNonEmptyResponseMessageContent(record);
|
|
3184
|
-
});
|
|
3185
|
-
}
|
|
3186
|
-
|
|
3187
|
-
export async function requestOpenAINativeCompaction(
|
|
3188
|
-
model: Model<Api>,
|
|
3189
|
-
context: Context,
|
|
3190
|
-
options: {
|
|
3191
|
-
mode: "responses" | "responses-compact";
|
|
3192
|
-
apiKey: string;
|
|
3193
|
-
headers?: ProviderHeaders;
|
|
3194
|
-
signal?: AbortSignal;
|
|
3195
|
-
reasoning?: SimpleStreamOptions["reasoning"];
|
|
3196
|
-
sessionId?: string;
|
|
3197
|
-
turnId?: string;
|
|
3198
|
-
maxRetries?: number;
|
|
3199
|
-
maxRetryDelayMs?: number;
|
|
3200
|
-
settings: ResolvedCodexModelSettings;
|
|
3201
|
-
},
|
|
3202
|
-
): Promise<unknown[]> {
|
|
3203
|
-
const settings = options.settings.modelProfile
|
|
3204
|
-
? options.settings
|
|
3205
|
-
: loadModelSettings(model, undefined, options.settings);
|
|
3206
|
-
const auth = { apiKey: options.apiKey || undefined, headers: options.headers };
|
|
3207
|
-
if (!hasCodexRequestAuth({ modelHeaders: model.headers, auth })) {
|
|
3208
|
-
throw new Error(`No request authentication for provider: ${model.provider}`);
|
|
3209
|
-
}
|
|
3210
|
-
if (settings.compactionMode === "pi") {
|
|
3211
|
-
throw new Error("native compaction is disabled by the current model profile");
|
|
3212
|
-
}
|
|
3213
|
-
const profile = resolveCodexRequestProfile(settings.requestProfile);
|
|
3214
|
-
const accountId = resolveCodexRequestAccountId({
|
|
3215
|
-
modelHeaders: model.headers,
|
|
3216
|
-
auth,
|
|
3217
|
-
apiKeyMode: settings.apiKeyMode,
|
|
3218
|
-
});
|
|
3219
|
-
const requestIdentity = resolveCodexRequestIdentity(
|
|
3220
|
-
options.sessionId,
|
|
3221
|
-
options.turnId ? { turn_id: options.turnId } : undefined,
|
|
3222
|
-
"compaction",
|
|
3223
|
-
);
|
|
3224
|
-
let body = applyFastModeServiceTier(buildRequestBody(model, context, profile, {
|
|
3225
|
-
apiKey: options.apiKey,
|
|
3226
|
-
headers: options.headers,
|
|
3227
|
-
signal: options.signal,
|
|
3228
|
-
reasoning: options.reasoning,
|
|
3229
|
-
sessionId: options.sessionId,
|
|
3230
|
-
}), settings, model);
|
|
3231
|
-
if (settings.nativeProviderTools) {
|
|
3232
|
-
const webSearch = settings.modelProfile?.effective.tools.webSearch;
|
|
3233
|
-
body = rewriteNativeOpenAiTools(body, {
|
|
3234
|
-
imageModel: settings.imageModel,
|
|
3235
|
-
imageGeneration: settings.imageGenerationImplementation ?? false,
|
|
3236
|
-
webSearch: settings.webSearchEnabled
|
|
3237
|
-
&& webSearch
|
|
3238
|
-
? {
|
|
3239
|
-
implementation: webSearch.implementation,
|
|
3240
|
-
contentTypes: webSearch.contentTypes,
|
|
3241
|
-
}
|
|
3242
|
-
: false,
|
|
3243
|
-
}).payload;
|
|
3244
|
-
}
|
|
3245
|
-
ensureWebSearchDetailsIncluded(body);
|
|
3246
|
-
|
|
3247
|
-
if (options.mode === "responses") {
|
|
3248
|
-
const retainedInput = [...body.input];
|
|
3249
|
-
body.input = [...retainedInput, { type: CODEX_COMPACTION_TRIGGER_TYPE }];
|
|
3250
|
-
const sseHeaders = applyConfiguredResponsesFeatureHeaders(buildSSEHeaders(
|
|
3251
|
-
model.headers,
|
|
3252
|
-
options.headers,
|
|
3253
|
-
accountId,
|
|
3254
|
-
options.apiKey,
|
|
3255
|
-
options.sessionId,
|
|
3256
|
-
profile,
|
|
3257
|
-
requestIdentity?.threadId,
|
|
3258
|
-
requestIdentity,
|
|
3259
|
-
), settings, model);
|
|
3260
|
-
const requestId = requestIdentity?.threadId
|
|
3261
|
-
?? options.sessionId
|
|
3262
|
-
?? createCodexRequestId();
|
|
3263
|
-
const websocketHeaders = applyConfiguredResponsesFeatureHeaders(buildWebSocketHeaders(
|
|
3264
|
-
model.headers,
|
|
3265
|
-
options.headers,
|
|
3266
|
-
accountId,
|
|
3267
|
-
options.apiKey,
|
|
3268
|
-
requestId,
|
|
3269
|
-
requestId,
|
|
3270
|
-
requestIdentity,
|
|
3271
|
-
), settings, model);
|
|
3272
|
-
const item = await requestCodexCompactionTriggerWithTransport(
|
|
3273
|
-
model,
|
|
3274
|
-
{ sse: sseHeaders, websocket: websocketHeaders },
|
|
3275
|
-
body,
|
|
3276
|
-
{
|
|
3277
|
-
sessionId: options.sessionId,
|
|
3278
|
-
turnId: options.turnId,
|
|
3279
|
-
requestIdentity,
|
|
3280
|
-
signal: options.signal,
|
|
3281
|
-
settings,
|
|
3282
|
-
maxRetries: options.maxRetries,
|
|
3283
|
-
maxRetryDelayMs: options.maxRetryDelayMs,
|
|
3284
|
-
},
|
|
3285
|
-
);
|
|
3286
|
-
return buildCodexCompactionCheckpoint(retainedInput, item);
|
|
3287
|
-
}
|
|
3288
|
-
|
|
3289
|
-
const headers = buildJsonHeaders(
|
|
3290
|
-
model.headers,
|
|
3291
|
-
options.headers,
|
|
3292
|
-
accountId,
|
|
3293
|
-
options.apiKey,
|
|
3294
|
-
options.sessionId,
|
|
3295
|
-
requestIdentity,
|
|
3296
|
-
);
|
|
3297
|
-
if (profile.responsesMode === "lite") {
|
|
3298
|
-
setProviderGeneratedHeader(headers, X_OPENAI_INTERNAL_CODEX_RESPONSES_LITE, "true");
|
|
3299
|
-
}
|
|
3300
|
-
const compactBody: Record<string, unknown> = {
|
|
3301
|
-
model: body.model,
|
|
3302
|
-
input: body.input,
|
|
3303
|
-
parallel_tool_calls: body.parallel_tool_calls,
|
|
3304
|
-
};
|
|
3305
|
-
for (const key of ["instructions", "tools", "reasoning", "service_tier", "prompt_cache_key", "text"] as const) {
|
|
3306
|
-
if (body[key] !== undefined) compactBody[key] = body[key];
|
|
3307
|
-
}
|
|
3308
|
-
const response = await postJsonWithRetries(
|
|
3309
|
-
compactUrl(model.baseUrl, settings.apiKeyMode),
|
|
3310
|
-
headers,
|
|
3311
|
-
compactBody,
|
|
3312
|
-
options.signal,
|
|
3313
|
-
);
|
|
3314
|
-
const output = response.output;
|
|
3315
|
-
if (!Array.isArray(output) || output.length === 0) {
|
|
3316
|
-
throw new Error("OpenAI /responses/compact returned no replacement output");
|
|
3317
|
-
}
|
|
3318
|
-
const sanitizedOutput = sanitizeNativeCompactionOutput(output);
|
|
3319
|
-
if (compactionItems(sanitizedOutput).length === 0) {
|
|
3320
|
-
throw new Error("OpenAI /responses/compact output did not contain a compaction item");
|
|
3321
|
-
}
|
|
3322
|
-
return sanitizedOutput;
|
|
3323
|
-
}
|
|
3324
|
-
|
|
3325
|
-
async function processWebSocketStream<TApi extends Api>(
|
|
3326
|
-
url: string,
|
|
3327
|
-
body: ResponsesBody,
|
|
3328
|
-
headers: Headers,
|
|
3329
|
-
output: AssistantMessage,
|
|
3330
|
-
stream: AssistantMessageEventStream,
|
|
3331
|
-
model: Model<TApi>,
|
|
3332
|
-
onStart: () => void,
|
|
3333
|
-
options: SimpleStreamOptions | undefined,
|
|
3334
|
-
deps: {
|
|
3335
|
-
onImageSaved?: (savedImage: SavedGeneratedImage, imageData: { data: string; mimeType: string }) => void;
|
|
3336
|
-
},
|
|
3337
|
-
cwd: string,
|
|
3338
|
-
requestPrompt: string | undefined,
|
|
3339
|
-
webSearchCitationSources: ReadonlyArray<WebSearchCitationSource>,
|
|
3340
|
-
historicalCitationSources: ReadonlyArray<CitationSource>,
|
|
3341
|
-
requestMetadata: WebSocketRequestMetadata,
|
|
3342
|
-
profileHash?: string,
|
|
3343
|
-
startupPrewarm?: Promise<void>,
|
|
3344
|
-
): Promise<void> {
|
|
3345
|
-
let streamStarted = false;
|
|
3346
|
-
let disableCachedContext = false;
|
|
3347
|
-
let staleSocketRetried = false;
|
|
3348
|
-
let missingPreviousResponseRetried = false;
|
|
3349
|
-
|
|
3350
|
-
while (true) {
|
|
3351
|
-
if (startupPrewarm) {
|
|
3352
|
-
await startupPrewarm;
|
|
3353
|
-
startupPrewarm = undefined;
|
|
3354
|
-
}
|
|
3355
|
-
const cacheKey = webSocketCacheKey(
|
|
3356
|
-
options?.sessionId,
|
|
3357
|
-
model as Model<Api>,
|
|
3358
|
-
url,
|
|
3359
|
-
headers,
|
|
3360
|
-
profileHash,
|
|
3361
|
-
);
|
|
3362
|
-
const { socket, entry, release, reused } = await acquireWebSocket(
|
|
3363
|
-
url,
|
|
3364
|
-
headers,
|
|
3365
|
-
cacheKey,
|
|
3366
|
-
options?.sessionId,
|
|
3367
|
-
options?.signal,
|
|
3368
|
-
WEBSOCKET_CONNECT_TIMEOUT_MS,
|
|
3369
|
-
);
|
|
3370
|
-
let keepConnection = true;
|
|
3371
|
-
let released = false;
|
|
3372
|
-
let eventCount = 0;
|
|
3373
|
-
// Continuation is safe only when buildCachedWebSocketRequestBody proves
|
|
3374
|
-
// that this request exactly extends the cached logical request.
|
|
3375
|
-
const useCachedContext = true;
|
|
3376
|
-
// ChatGPT Codex Responses rejects `store: true` ("Store must be set to false").
|
|
3377
|
-
// WebSocket continuation still works via connection-scoped previous_response_id state.
|
|
3378
|
-
const fullBody = withWebSocketRequestMetadata(body, requestMetadata);
|
|
3379
|
-
const requestBody = useCachedContext && !disableCachedContext && entry
|
|
3380
|
-
? buildCachedWebSocketRequestBody(entry, fullBody)
|
|
3381
|
-
: fullBody;
|
|
3382
|
-
const wireRequestBody = prepareWebSocketRequestBodyForWire(requestBody);
|
|
3383
|
-
|
|
3384
|
-
const releaseOnce = (releaseOptions?: { keep?: boolean }) => {
|
|
3385
|
-
if (released) return;
|
|
3386
|
-
released = true;
|
|
3387
|
-
release(releaseOptions);
|
|
3388
|
-
};
|
|
3389
|
-
|
|
3390
|
-
try {
|
|
3391
|
-
await sendWebSocketRequest(
|
|
3392
|
-
socket,
|
|
3393
|
-
JSON.stringify({ type: "response.create", ...wireRequestBody }),
|
|
3394
|
-
options?.signal,
|
|
3395
|
-
WEBSOCKET_SEND_TIMEOUT_MS,
|
|
3396
|
-
);
|
|
3397
|
-
const startOutput = () => {
|
|
3398
|
-
if (streamStarted) return;
|
|
3399
|
-
onStart();
|
|
3400
|
-
stream.push({ type: "start", partial: output });
|
|
3401
|
-
streamStarted = true;
|
|
3402
|
-
};
|
|
3403
|
-
const continuationResult = await processCapturedResponsesStream(
|
|
3404
|
-
startWebSocketOutputOnFirstEvent(
|
|
3405
|
-
countWebSocketEvents(parseWebSocket(socket, options?.signal), () => {
|
|
3406
|
-
eventCount++;
|
|
3407
|
-
}),
|
|
3408
|
-
startOutput,
|
|
3409
|
-
),
|
|
3410
|
-
output,
|
|
3411
|
-
stream,
|
|
3412
|
-
model,
|
|
3413
|
-
options,
|
|
3414
|
-
options?.sessionId,
|
|
3415
|
-
deps,
|
|
3416
|
-
cwd,
|
|
3417
|
-
requestPrompt,
|
|
3418
|
-
webSearchCitationSources,
|
|
3419
|
-
historicalCitationSources,
|
|
3420
|
-
);
|
|
3421
|
-
if (options?.signal?.aborted) {
|
|
3422
|
-
keepConnection = false;
|
|
3423
|
-
} else if (entry && continuationResult.responseId) {
|
|
3424
|
-
entry.continuation = {
|
|
3425
|
-
lastRequestBody: fullBody,
|
|
3426
|
-
lastResponseId: continuationResult.responseId,
|
|
3427
|
-
lastResponseItems: continuationResult.responseItems,
|
|
3428
|
-
};
|
|
3429
|
-
} else if (entry) {
|
|
3430
|
-
entry.continuation = undefined;
|
|
3431
|
-
}
|
|
3432
|
-
releaseOnce({ keep: keepConnection });
|
|
3433
|
-
return;
|
|
3434
|
-
} catch (error) {
|
|
3435
|
-
if (entry) {
|
|
3436
|
-
entry.continuation = undefined;
|
|
3437
|
-
}
|
|
3438
|
-
keepConnection = false;
|
|
3439
|
-
releaseOnce({ keep: false });
|
|
3440
|
-
// Pi's stock provider reuses session WebSockets. In practice the Codex
|
|
3441
|
-
// backend sometimes cleanly closes an idle cached socket between turns;
|
|
3442
|
-
// if that stale socket fails before any response event, retry once on a
|
|
3443
|
-
// fresh WebSocket without changing request shape or falling back transports.
|
|
3444
|
-
if (!staleSocketRetried && reused && eventCount === 0 && !options?.signal?.aborted && isRetryableEarlyWebSocketError(error)) {
|
|
3445
|
-
staleSocketRetried = true;
|
|
3446
|
-
continue;
|
|
3447
|
-
}
|
|
3448
|
-
if (
|
|
3449
|
-
!missingPreviousResponseRetried
|
|
3450
|
-
&& requestBody.previous_response_id
|
|
3451
|
-
&& !streamStarted
|
|
3452
|
-
&& !options?.signal?.aborted
|
|
3453
|
-
&& isPreviousResponseNotFoundError(error)
|
|
3454
|
-
) {
|
|
3455
|
-
missingPreviousResponseRetried = true;
|
|
3456
|
-
disableCachedContext = true;
|
|
3457
|
-
continue;
|
|
3458
|
-
}
|
|
3459
|
-
throw error;
|
|
3460
|
-
} finally {
|
|
3461
|
-
releaseOnce({ keep: keepConnection });
|
|
3462
|
-
}
|
|
3463
|
-
}
|
|
3464
|
-
}
|
|
3465
|
-
|
|
3466
|
-
export function extractWebSearch(
|
|
3467
|
-
item: StreamEventShape["item"],
|
|
3468
|
-
options?: { completed?: boolean },
|
|
3469
|
-
): SurfacedWebSearch | undefined {
|
|
3470
|
-
if (!item || item.type !== "web_search_call") return undefined;
|
|
3471
|
-
const callId = typeof item.id === "string" ? item.id : typeof item.call_id === "string" ? item.call_id : undefined;
|
|
3472
|
-
if (!callId) return undefined;
|
|
3473
|
-
|
|
3474
|
-
const action = typeof item.action === "object" && item.action !== null ? (item.action as Record<string, unknown>) : undefined;
|
|
3475
|
-
const actionType = typeof action?.type === "string" ? action.type : undefined;
|
|
3476
|
-
const query = typeof action?.query === "string" ? action.query : typeof item.query === "string" ? item.query : undefined;
|
|
3477
|
-
const queries = [
|
|
3478
|
-
...(Array.isArray(action?.queries) ? action.queries : []),
|
|
3479
|
-
...(Array.isArray(item.queries) ? item.queries : []),
|
|
3480
|
-
].filter((value): value is string => typeof value === "string" && value.trim().length > 0);
|
|
3481
|
-
const url = typeof action?.url === "string" && action.url.trim()
|
|
3482
|
-
? action.url.trim()
|
|
3483
|
-
: typeof item.url === "string" && item.url.trim()
|
|
3484
|
-
? item.url.trim()
|
|
3485
|
-
: undefined;
|
|
3486
|
-
const pattern = typeof action?.pattern === "string" && action.pattern.trim() ? action.pattern.trim() : undefined;
|
|
3487
|
-
|
|
3488
|
-
const asRecordArray = (value: unknown): Record<string, unknown>[] => Array.isArray(value)
|
|
3489
|
-
? value
|
|
3490
|
-
.map((entry) => typeof entry === "object" && entry !== null ? entry as Record<string, unknown> : undefined)
|
|
3491
|
-
.filter((entry): entry is Record<string, unknown> => !!entry)
|
|
3492
|
-
: [];
|
|
3493
|
-
const sourceCandidates = [
|
|
3494
|
-
...asRecordArray(action?.sources),
|
|
3495
|
-
...asRecordArray(action?.results),
|
|
3496
|
-
...asRecordArray(item.results),
|
|
3497
|
-
];
|
|
3498
|
-
if (typeof item.url === "string") sourceCandidates.push(item as Record<string, unknown>);
|
|
3499
|
-
|
|
3500
|
-
const seenUrls = new Set<string>();
|
|
3501
|
-
const sources: Array<{ title?: string; url: string }> = [];
|
|
3502
|
-
for (const source of sourceCandidates) {
|
|
3503
|
-
const url = typeof source.url === "string" && source.url.trim() ? source.url.trim() : undefined;
|
|
3504
|
-
if (!url || seenUrls.has(url)) continue;
|
|
3505
|
-
seenUrls.add(url);
|
|
3506
|
-
const title = typeof source.title === "string" && source.title.trim() ? source.title.trim() : undefined;
|
|
3507
|
-
sources.push({ ...(title ? { title } : {}), url });
|
|
3508
|
-
}
|
|
3509
|
-
|
|
3510
|
-
return {
|
|
3511
|
-
callId,
|
|
3512
|
-
...(typeof item.status === "string" ? { status: item.status } : {}),
|
|
3513
|
-
...(options?.completed !== undefined ? { completed: options.completed } : {}),
|
|
3514
|
-
...(actionType ? { actionType } : {}),
|
|
3515
|
-
...(query ? { query } : {}),
|
|
3516
|
-
queries,
|
|
3517
|
-
...(url ? { url } : {}),
|
|
3518
|
-
...(pattern ? { pattern } : {}),
|
|
3519
|
-
sources,
|
|
3520
|
-
...(options?.completed ? { responseItem: item as Record<string, unknown> } : {}),
|
|
3521
|
-
};
|
|
3522
|
-
}
|
|
3523
|
-
|
|
3524
|
-
export function extractWebSearchProgress(event: StreamEventShape): SurfacedWebSearch | undefined {
|
|
3525
|
-
const status = event.type === "response.web_search_call.in_progress"
|
|
3526
|
-
? "in_progress"
|
|
3527
|
-
: event.type === "response.web_search_call.searching"
|
|
3528
|
-
? "searching"
|
|
3529
|
-
: event.type === "response.web_search_call.completed"
|
|
3530
|
-
? "completed"
|
|
3531
|
-
: undefined;
|
|
3532
|
-
if (!status || typeof event.item_id !== "string" || !event.item_id) return undefined;
|
|
3533
|
-
return {
|
|
3534
|
-
callId: event.item_id,
|
|
3535
|
-
status,
|
|
3536
|
-
completed: status === "completed",
|
|
3537
|
-
queries: [],
|
|
3538
|
-
sources: [],
|
|
3539
|
-
};
|
|
3540
|
-
}
|
|
3541
|
-
|
|
3542
|
-
export function mergeWebSearchActivity(
|
|
3543
|
-
previous: SurfacedWebSearch | undefined,
|
|
3544
|
-
next: SurfacedWebSearch,
|
|
3545
|
-
): SurfacedWebSearch {
|
|
3546
|
-
if (!previous) return next;
|
|
3547
|
-
const seenUrls = new Set<string>();
|
|
3548
|
-
const sources = [...next.sources, ...previous.sources].filter((source) => {
|
|
3549
|
-
if (seenUrls.has(source.url)) return false;
|
|
3550
|
-
seenUrls.add(source.url);
|
|
3551
|
-
return true;
|
|
3552
|
-
});
|
|
3553
|
-
const completed = Boolean(previous.completed || next.completed);
|
|
3554
|
-
return {
|
|
3555
|
-
callId: next.callId,
|
|
3556
|
-
status: previous.completed && !next.completed ? previous.status : (next.status ?? previous.status),
|
|
3557
|
-
completed,
|
|
3558
|
-
actionType: next.actionType ?? previous.actionType,
|
|
3559
|
-
query: next.query ?? previous.query,
|
|
3560
|
-
queries: next.queries.length > 0 ? next.queries : previous.queries,
|
|
3561
|
-
url: next.url ?? previous.url,
|
|
3562
|
-
pattern: next.pattern ?? previous.pattern,
|
|
3563
|
-
sources,
|
|
3564
|
-
responseItem: next.responseItem ?? previous.responseItem,
|
|
3565
|
-
};
|
|
3566
|
-
}
|
|
3567
|
-
|
|
3568
|
-
export function webSearchActivityDetail(search: SurfacedWebSearch): string {
|
|
3569
|
-
if (search.actionType === "open_page") return search.url ?? "";
|
|
3570
|
-
if (search.actionType === "find_in_page") {
|
|
3571
|
-
if (search.pattern && search.url) return `'${search.pattern}' in ${search.url}`;
|
|
3572
|
-
if (search.pattern) return `'${search.pattern}'`;
|
|
3573
|
-
return search.url ?? "";
|
|
3574
|
-
}
|
|
3575
|
-
const query = search.query?.trim();
|
|
3576
|
-
if (query) return query;
|
|
3577
|
-
const first = search.queries[0]?.trim() ?? "";
|
|
3578
|
-
return search.queries.length > 1 && first ? `${first} ...` : first;
|
|
3579
|
-
}
|
|
3580
|
-
|
|
3581
|
-
export function webSearchActivityHosts(search: SurfacedWebSearch): string[] {
|
|
3582
|
-
const seen = new Set<string>();
|
|
3583
|
-
const hosts: string[] = [];
|
|
3584
|
-
for (const source of search.sources) {
|
|
3585
|
-
try {
|
|
3586
|
-
const host = new URL(source.url).hostname.replace(/^www\./i, "");
|
|
3587
|
-
const key = host.toLowerCase();
|
|
3588
|
-
if (!host || seen.has(key)) continue;
|
|
3589
|
-
seen.add(key);
|
|
3590
|
-
hosts.push(host);
|
|
3591
|
-
} catch {
|
|
3592
|
-
// Sources without a valid URL do not produce a host tag.
|
|
3593
|
-
}
|
|
3594
|
-
}
|
|
3595
|
-
return hosts;
|
|
3596
|
-
}
|
|
3597
|
-
|
|
3598
|
-
export function buildWebSearchStatusText(search: SurfacedWebSearch): string {
|
|
3599
|
-
const completed = search.completed ?? search.status === "completed";
|
|
3600
|
-
const detail = webSearchActivityDetail(search);
|
|
3601
|
-
if (completed) return `Searched the web${detail ? ` for ${detail}` : ""}`;
|
|
3602
|
-
return `Searching the web${detail ? ` ${detail}` : ""}`;
|
|
3603
|
-
}
|
|
3604
|
-
|
|
3605
|
-
export function buildWebSearchInlineText(search: SurfacedWebSearch, cwd?: string): string {
|
|
3606
|
-
const completed = search.completed ?? search.status === "completed";
|
|
3607
|
-
const header = completed ? "Searched the web" : "Searching the web";
|
|
3608
|
-
const detail = webSearchActivityDetail(search);
|
|
3609
|
-
const separator = detail ? (completed ? " for " : " ") : "";
|
|
3610
|
-
return `${glyphs(cwd).bullet}**${header}**${separator}${detail}`;
|
|
3611
|
-
}
|
|
3612
|
-
|
|
3613
|
-
export function buildWebSearchActivityMessage(searches: SurfacedWebSearch[]): string {
|
|
3614
|
-
const sections = searches.map((search, index) => {
|
|
3615
|
-
const heading = searches.length > 1
|
|
3616
|
-
? `${index + 1}. ${buildWebSearchStatusText(search)}`
|
|
3617
|
-
: buildWebSearchStatusText(search);
|
|
3618
|
-
const lines = [heading, `Call: ${search.callId}${search.status ? ` (${search.status})` : ""}`];
|
|
3619
|
-
const queries = search.queries.length > 0 ? search.queries : search.query ? [search.query] : [];
|
|
3620
|
-
if (queries.length > 0) {
|
|
3621
|
-
lines.push(`Query: ${queries.join(" | ")}`);
|
|
3622
|
-
}
|
|
3623
|
-
if (search.sources.length > 0) {
|
|
3624
|
-
lines.push("Sources:");
|
|
3625
|
-
for (const source of search.sources.slice(0, 8)) {
|
|
3626
|
-
lines.push(`- ${source.title ? `${source.title}: ` : ""}${source.url}`);
|
|
3627
|
-
}
|
|
3628
|
-
}
|
|
3629
|
-
return lines.join("\n");
|
|
3630
|
-
});
|
|
3631
|
-
|
|
3632
|
-
return sections.join("\n\n");
|
|
3633
|
-
}
|
|
3634
|
-
|
|
3635
|
-
export function buildWebSearchSummaryText(searches: SurfacedWebSearch[]): string {
|
|
3636
|
-
if (searches.length === 0) return "Web search";
|
|
3637
|
-
if (searches.length === 1) return buildWebSearchStatusText(searches[0]!);
|
|
3638
|
-
const completed = searches.filter((search) => search.completed ?? search.status === "completed").length;
|
|
3639
|
-
if (completed === searches.length) return `Searched the web ${searches.length} times`;
|
|
3640
|
-
if (completed === 0) return `Searching the web (${searches.length} calls)`;
|
|
3641
|
-
return `Web search activity (${completed}/${searches.length} completed)`;
|
|
3642
|
-
}
|
|
3643
|
-
|
|
3644
|
-
function makeCachedImagePreview(data: string, mimeType: string, bytes?: number): CachedImagePreview {
|
|
3645
|
-
const dimensions = getImageDimensions(data, mimeType) ?? undefined;
|
|
3646
|
-
return { data, mimeType, bytes: bytes ?? Buffer.from(data, "base64").byteLength, widthPx: dimensions?.widthPx, heightPx: dimensions?.heightPx };
|
|
3647
|
-
}
|
|
3648
|
-
|
|
3649
|
-
function loadCachedImagePreview(savedImage: SavedGeneratedImage, imagePreviewCache: Map<string, CachedImagePreview>): CachedImagePreview | undefined {
|
|
3650
|
-
const cached = imagePreviewCache.get(savedImage.absolutePath);
|
|
3651
|
-
if (cached) return cached;
|
|
3652
|
-
const fs = getNodeFsSync();
|
|
3653
|
-
if (!fs) return undefined;
|
|
3654
|
-
try {
|
|
3655
|
-
const buffer = fs.readFileSync(savedImage.absolutePath);
|
|
3656
|
-
const data = buffer.toString("base64");
|
|
3657
|
-
const mimeType = `image/${savedImage.outputFormat}`;
|
|
3658
|
-
const preview = makeCachedImagePreview(data, mimeType, buffer.byteLength);
|
|
3659
|
-
imagePreviewCache.set(savedImage.absolutePath, preview);
|
|
3660
|
-
return preview;
|
|
3661
|
-
} catch {
|
|
3662
|
-
return undefined;
|
|
3663
|
-
}
|
|
3664
|
-
}
|
|
3665
|
-
|
|
3666
|
-
function formatImageBytes(bytes: number | undefined): string | undefined {
|
|
3667
|
-
if (!Number.isFinite(bytes) || !bytes) return undefined;
|
|
3668
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
3669
|
-
if (bytes < 1024 * 1024) return `${Math.round(bytes / 102.4) / 10}K`;
|
|
3670
|
-
return `${Math.round(bytes / (1024 * 102.4)) / 10}M`;
|
|
3671
|
-
}
|
|
3672
|
-
|
|
3673
|
-
function themeFg(theme: any, token: string, text: string): string {
|
|
3674
|
-
try { return theme?.fg?.(token, text) ?? text; } catch { return text; }
|
|
3675
|
-
}
|
|
3676
|
-
|
|
3677
|
-
function themeBold(theme: any, text: string): string {
|
|
3678
|
-
try { return theme?.bold?.(text) ?? text; } catch { return text; }
|
|
3679
|
-
}
|
|
3680
|
-
|
|
3681
|
-
function shouldRenderInlineImage(): { ok: boolean; reason?: string } {
|
|
3682
|
-
if (process.env.TMUX) return { ok: false, reason: "inline preview disabled in tmux to avoid overlay/stale image artifacts" };
|
|
3683
|
-
const protocol = getCapabilities().images;
|
|
3684
|
-
if (!protocol) return { ok: false, reason: "terminal image protocol unavailable" };
|
|
3685
|
-
return { ok: true };
|
|
3686
|
-
}
|
|
3687
|
-
|
|
3688
|
-
function renderImageGenerationMessage(savedImage: SavedGeneratedImage | undefined, messageContent: unknown, options: any, theme: any, imagePreviewCache: Map<string, CachedImagePreview>): Container {
|
|
3689
|
-
const container = new Container();
|
|
3690
|
-
const preview = savedImage ? loadCachedImagePreview(savedImage, imagePreviewCache) : undefined;
|
|
3691
|
-
const type = savedImage?.outputFormat?.toUpperCase() ?? preview?.mimeType?.replace(/^image\//, "").toUpperCase() ?? "IMAGE";
|
|
3692
|
-
const dimensions = preview?.widthPx && preview?.heightPx ? `${preview.widthPx}x${preview.heightPx}` : undefined;
|
|
3693
|
-
const size = formatImageBytes(preview?.bytes);
|
|
3694
|
-
const imageModel = savedImage?.imageModel ? `model ${savedImage.imageModel}` : undefined;
|
|
3695
|
-
const meta = [imageModel, type, dimensions, size].filter(Boolean).join(glyphs().dot);
|
|
3696
|
-
const label = `${themeFg(theme, "accent", glyphs().bullet)}${themeFg(theme, "text", themeBold(theme, "Image Generation "))}`;
|
|
3697
|
-
const pathText = savedImage?.relativePath ?? (typeof messageContent === "string" ? messageContent : "generated image");
|
|
3698
|
-
const lines = [`${label}${themeFg(theme, "accent", pathText)}${meta ? themeFg(theme, "dim", `${glyphs().dot}${meta}`) : ""}`];
|
|
3699
|
-
if (savedImage?.latestRelativePath) lines.push(`${themeFg(theme, "muted", ` ${treeGlyph("├")}`)}${themeFg(theme, "text", "Latest ")}${themeFg(theme, "accent", savedImage.latestRelativePath)}`);
|
|
3700
|
-
if (options?.expanded && savedImage?.revisedPrompt) lines.push(`${themeFg(theme, "muted", ` ${treeGlyph("├")}`)}${themeFg(theme, "text", "Prompt ")}${themeFg(theme, "dim", savedImage.revisedPrompt)}`);
|
|
3701
|
-
const inline = shouldRenderInlineImage();
|
|
3702
|
-
if (!inline.ok) lines.push(`${themeFg(theme, "muted", ` ${treeGlyph("└")}`)}${themeFg(theme, "warning", inline.reason ?? "inline preview unavailable")}`);
|
|
3703
|
-
container.addChild(new Text(lines.join("\n"), 0, 0));
|
|
3704
|
-
if (savedImage && preview && inline.ok) {
|
|
3705
|
-
container.addChild(new Spacer(1));
|
|
3706
|
-
container.addChild(new Image(preview.data, preview.mimeType, { fallbackColor: (text) => themeFg(theme, "dim", text) }, { maxWidthCells: 72, maxHeightCells: options?.expanded ? 24 : 14, filename: savedImage.relativePath }));
|
|
3707
|
-
}
|
|
3708
|
-
return container;
|
|
3709
|
-
}
|
|
3710
|
-
|
|
3711
|
-
function createInitialAssistantMessage<TApi extends Api>(model: Model<TApi>): AssistantMessage {
|
|
3712
|
-
return {
|
|
3713
|
-
role: "assistant",
|
|
3714
|
-
content: [],
|
|
3715
|
-
api: model.api,
|
|
3716
|
-
provider: model.provider,
|
|
3717
|
-
model: model.id,
|
|
3718
|
-
usage: {
|
|
3719
|
-
input: 0,
|
|
3720
|
-
output: 0,
|
|
3721
|
-
cacheRead: 0,
|
|
3722
|
-
cacheWrite: 0,
|
|
3723
|
-
totalTokens: 0,
|
|
3724
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
3725
|
-
},
|
|
3726
|
-
stopReason: "stop",
|
|
3727
|
-
timestamp: Date.now(),
|
|
3728
|
-
};
|
|
3729
|
-
}
|
|
3730
|
-
|
|
3731
|
-
function createErrorMessage(message: AssistantMessage, error: unknown, aborted: boolean): AssistantMessage {
|
|
3732
|
-
for (const block of message.content) {
|
|
3733
|
-
if (typeof block === "object" && block !== null && "partialJson" in block) {
|
|
3734
|
-
delete (block as { partialJson?: string }).partialJson;
|
|
3735
|
-
}
|
|
3736
|
-
}
|
|
3737
|
-
message.stopReason = aborted ? "aborted" : "error";
|
|
3738
|
-
message.errorMessage = buildProviderErrorMessage(error);
|
|
3739
|
-
return message;
|
|
3740
|
-
}
|
|
3741
|
-
|
|
3742
|
-
export function buildProviderErrorMessage(error: unknown): string {
|
|
3743
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3744
|
-
const candidate = error as { code?: unknown; errorType?: unknown; status?: unknown };
|
|
3745
|
-
if (
|
|
3746
|
-
candidate?.code === "stream_read_error"
|
|
3747
|
-
&& candidate.errorType === "upstream_error"
|
|
3748
|
-
) {
|
|
3749
|
-
// Pi's agent-level retry classifier recognizes "connection error". Surface
|
|
3750
|
-
// this upstream SSE read failure in that category instead of
|
|
3751
|
-
// retrying inside the provider, so Pi's retry settings and UI remain the
|
|
3752
|
-
// single source of truth.
|
|
3753
|
-
return `Connection error: ${message}`;
|
|
3754
|
-
}
|
|
3755
|
-
if (/^(?:WebSocket (?:error|closed)|WebSocket stream closed before response\.completed|Stream closed before response\.completed)/.test(message)) {
|
|
3756
|
-
return `Connection error: ${message}`;
|
|
3757
|
-
}
|
|
3758
|
-
if (typeof candidate?.status === "number") {
|
|
3759
|
-
return withHttpStatusPrefix(candidate.status, message);
|
|
3760
|
-
}
|
|
3761
|
-
return message;
|
|
3762
|
-
}
|
|
3763
|
-
|
|
3764
|
-
function finalizeUsage<TApi extends Api>(model: Model<TApi>, output: AssistantMessage): void {
|
|
3765
|
-
output.usage.cost.total = output.usage.cost.input + output.usage.cost.output + output.usage.cost.cacheRead + output.usage.cost.cacheWrite;
|
|
3766
|
-
}
|
|
3767
|
-
|
|
3768
|
-
async function parseErrorResponse(response: Response): Promise<{ message: string; friendlyMessage?: string }> {
|
|
3769
|
-
const raw = await response.text();
|
|
3770
|
-
let message = raw || response.statusText || "Request failed";
|
|
3771
|
-
let friendlyMessage: string | undefined;
|
|
3772
|
-
|
|
3773
|
-
try {
|
|
3774
|
-
const parsed = JSON.parse(raw) as { error?: { code?: string; type?: string; plan_type?: string; resets_at?: number; message?: string } };
|
|
3775
|
-
const err = parsed?.error;
|
|
3776
|
-
if (err) {
|
|
3777
|
-
const code = err.code || err.type || "";
|
|
3778
|
-
if (/usage_limit_reached|usage_not_included|rate_limit_exceeded/i.test(code) || response.status === 429) {
|
|
3779
|
-
const plan = err.plan_type ? ` (${err.plan_type.toLowerCase()} plan)` : "";
|
|
3780
|
-
const mins = err.resets_at ? Math.max(0, Math.round((err.resets_at * 1000 - Date.now()) / 60000)) : undefined;
|
|
3781
|
-
const when = mins !== undefined ? ` Try again in ~${mins} min.` : "";
|
|
3782
|
-
friendlyMessage = `You have hit your ChatGPT usage limit${plan}.${when}`.trim();
|
|
3783
|
-
}
|
|
3784
|
-
message = err.message || friendlyMessage || message;
|
|
3785
|
-
}
|
|
3786
|
-
} catch {
|
|
3787
|
-
// ignore malformed error bodies
|
|
3788
|
-
}
|
|
3789
|
-
|
|
3790
|
-
return { message, friendlyMessage };
|
|
3791
|
-
}
|
|
3792
|
-
|
|
3793
|
-
function createCodexStream<TApi extends Api>(
|
|
3794
|
-
model: Model<TApi>,
|
|
3795
|
-
context: Context,
|
|
3796
|
-
options: SimpleStreamOptions | undefined,
|
|
3797
|
-
deps: {
|
|
3798
|
-
getCurrentCwd: () => string;
|
|
3799
|
-
getCurrentTurnId?: (sessionId: string | undefined) => string | undefined;
|
|
3800
|
-
getStartupPrewarm?: (sessionId: string, model: Model<Api>) => Promise<void> | undefined;
|
|
3801
|
-
onImageSaved?: (savedImage: SavedGeneratedImage, imageData: { data: string; mimeType: string }) => void;
|
|
3802
|
-
},
|
|
3803
|
-
): AssistantMessageEventStream {
|
|
3804
|
-
const stream = createAssistantMessageEventStream();
|
|
3805
|
-
const requestCwd = deps.getCurrentCwd();
|
|
3806
|
-
|
|
3807
|
-
(async () => {
|
|
3808
|
-
const output = createInitialAssistantMessage(model);
|
|
3809
|
-
const requestPrompt = getLatestUserText(context);
|
|
3810
|
-
const webSearchCitationSources = collectWebSearchCitationSources(model, context);
|
|
3811
|
-
const historicalCitationSources = collectHistoricalCitationSources(model, context);
|
|
3812
|
-
|
|
3813
|
-
try {
|
|
3814
|
-
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
3815
|
-
const requestHeaders = options?.headers;
|
|
3816
|
-
const auth = { apiKey: apiKey || undefined, headers: requestHeaders };
|
|
3817
|
-
if (!hasCodexRequestAuth({ modelHeaders: model.headers, auth })) {
|
|
3818
|
-
throw new Error(`No request authentication for provider: ${model.provider}`);
|
|
3819
|
-
}
|
|
3820
|
-
|
|
3821
|
-
const settings = loadModelSettings(model, requestCwd);
|
|
3822
|
-
const requestProfile = resolveCodexRequestProfile(settings.requestProfile);
|
|
3823
|
-
if (
|
|
3824
|
-
!settings.enabled
|
|
3825
|
-
|| !settings.modelProfile?.effective.enabled
|
|
3826
|
-
|| !settings.providerShimActive
|
|
3827
|
-
) {
|
|
3828
|
-
throw new Error(`No enabled Codex model profile for ${model.provider}/${model.id}`);
|
|
3829
|
-
}
|
|
3830
|
-
const apiKeyTransport = settings.apiKeyMode;
|
|
3831
|
-
const accountId = resolveCodexRequestAccountId({
|
|
3832
|
-
modelHeaders: model.headers,
|
|
3833
|
-
auth,
|
|
3834
|
-
apiKeyMode: apiKeyTransport,
|
|
3835
|
-
});
|
|
3836
|
-
const requestIdentity = resolveCodexRequestIdentity(
|
|
3837
|
-
options?.sessionId,
|
|
3838
|
-
options?.metadata as Record<string, unknown> | undefined,
|
|
3839
|
-
"turn",
|
|
3840
|
-
);
|
|
3841
|
-
let body = applyFastModeServiceTier(
|
|
3842
|
-
buildRequestBody(model, context, requestProfile, options),
|
|
3843
|
-
settings,
|
|
3844
|
-
model,
|
|
3845
|
-
);
|
|
3846
|
-
if (settings.nativeProviderTools) {
|
|
3847
|
-
const webSearch = settings.modelProfile.effective.tools.webSearch;
|
|
3848
|
-
body = rewriteNativeOpenAiTools(body, {
|
|
3849
|
-
imageModel: settings.imageModel,
|
|
3850
|
-
imageGeneration: settings.imageGenerationImplementation ?? false,
|
|
3851
|
-
webSearch: settings.webSearchEnabled && webSearch
|
|
3852
|
-
? {
|
|
3853
|
-
implementation: webSearch.implementation,
|
|
3854
|
-
contentTypes: webSearch.contentTypes,
|
|
3855
|
-
}
|
|
3856
|
-
: false,
|
|
3857
|
-
}).payload;
|
|
3858
|
-
}
|
|
3859
|
-
const nextBody = await options?.onPayload?.(body, model);
|
|
3860
|
-
if (nextBody !== undefined) {
|
|
3861
|
-
body = nextBody as ResponsesBody;
|
|
3862
|
-
}
|
|
3863
|
-
options = withRequestServiceTier(options, body.service_tier);
|
|
3864
|
-
ensureWebSearchDetailsIncluded(body);
|
|
3865
|
-
|
|
3866
|
-
const websocketSessionId = requestIdentity?.sessionId ?? options?.sessionId;
|
|
3867
|
-
const websocketThreadId = requestIdentity?.threadId ?? options?.sessionId;
|
|
3868
|
-
const websocketTurnId = requestIdentity?.turnId
|
|
3869
|
-
|| deps.getCurrentTurnId?.(options?.sessionId)
|
|
3870
|
-
|| createPiTurnId();
|
|
3871
|
-
const websocketRequestId = websocketThreadId
|
|
3872
|
-
|| websocketSessionId
|
|
3873
|
-
|| createCodexRequestId();
|
|
3874
|
-
const websocketRequestMetadata: WebSocketRequestMetadata = {
|
|
3875
|
-
...(options?.sessionId ? { sessionId: options.sessionId } : {}),
|
|
3876
|
-
...(websocketThreadId ? { threadId: websocketThreadId } : {}),
|
|
3877
|
-
turnId: websocketTurnId,
|
|
3878
|
-
...(requestIdentity ? { identity: requestIdentity } : {}),
|
|
3879
|
-
};
|
|
3880
|
-
let sseHeaders = applyConfiguredResponsesFeatureHeaders(
|
|
3881
|
-
buildSSEHeaders(
|
|
3882
|
-
model.headers,
|
|
3883
|
-
requestHeaders,
|
|
3884
|
-
accountId,
|
|
3885
|
-
apiKey,
|
|
3886
|
-
options?.sessionId,
|
|
3887
|
-
requestProfile,
|
|
3888
|
-
websocketThreadId,
|
|
3889
|
-
requestIdentity,
|
|
3890
|
-
),
|
|
3891
|
-
settings,
|
|
3892
|
-
model as Model<Api>,
|
|
3893
|
-
);
|
|
3894
|
-
let websocketHeaders = applyConfiguredResponsesFeatureHeaders(buildWebSocketHeaders(
|
|
3895
|
-
model.headers,
|
|
3896
|
-
requestHeaders,
|
|
3897
|
-
accountId,
|
|
3898
|
-
apiKey,
|
|
3899
|
-
options?.sessionId ?? websocketRequestId,
|
|
3900
|
-
websocketThreadId ?? websocketRequestId,
|
|
3901
|
-
requestIdentity,
|
|
3902
|
-
), settings, model as Model<Api>);
|
|
3903
|
-
const transformHeaders = (
|
|
3904
|
-
options as
|
|
3905
|
-
| (SimpleStreamOptions & {
|
|
3906
|
-
transformHeaders?: (
|
|
3907
|
-
headers: ProviderHeaders,
|
|
3908
|
-
) => ProviderHeaders | Promise<ProviderHeaders>;
|
|
3909
|
-
})
|
|
3910
|
-
| undefined
|
|
3911
|
-
)?.transformHeaders;
|
|
3912
|
-
const bodyJson = JSON.stringify(withSseRequestMetadata(body, websocketRequestMetadata));
|
|
3913
|
-
const responseHeaderTimeoutMs = responseHeaderTimeoutMsFromOptions(options);
|
|
3914
|
-
const configuredTransport: ProviderTransport = settings.openaiTransport;
|
|
3915
|
-
// Pi exposes a session transport setting through stream options. Treat
|
|
3916
|
-
// explicit non-auto values as overrides; otherwise use the model profile.
|
|
3917
|
-
const transport: ProviderTransport = options?.transport && options.transport !== "auto"
|
|
3918
|
-
? options.transport
|
|
3919
|
-
: configuredTransport;
|
|
3920
|
-
|
|
3921
|
-
const websocketUrl = resolveResponsesWebSocketUrl(model.baseUrl, { apiKeyMode: apiKeyTransport });
|
|
3922
|
-
const fallbackKey = webSocketFallbackKey(
|
|
3923
|
-
options?.sessionId,
|
|
3924
|
-
model as Model<Api>,
|
|
3925
|
-
websocketUrl,
|
|
3926
|
-
settings.modelProfileHash,
|
|
3927
|
-
);
|
|
3928
|
-
const sessionFellBackToHttp = transport === "auto"
|
|
3929
|
-
&& fallbackKey !== undefined
|
|
3930
|
-
&& websocketHttpFallbackSessions.has(fallbackKey);
|
|
3931
|
-
|
|
3932
|
-
if (transport !== "sse" && !sessionFellBackToHttp) {
|
|
3933
|
-
if (transformHeaders) {
|
|
3934
|
-
websocketHeaders = providerHeadersToHeaders(
|
|
3935
|
-
await transformHeaders(
|
|
3936
|
-
headersToRecord(websocketHeaders),
|
|
3937
|
-
),
|
|
3938
|
-
);
|
|
3939
|
-
}
|
|
3940
|
-
const startupPrewarmTask = options?.sessionId
|
|
3941
|
-
? deps.getStartupPrewarm?.(options.sessionId, model as Model<Api>)
|
|
3942
|
-
: undefined;
|
|
3943
|
-
const websocketBody = withResponsesLiteWebSocketMetadata(body, requestProfile.responsesMode);
|
|
3944
|
-
let websocketStarted = false;
|
|
3945
|
-
let websocketRetries = 0;
|
|
3946
|
-
const maxWebSocketRetries = webSocketStreamMaxRetries(options);
|
|
3947
|
-
while (true) {
|
|
3948
|
-
websocketStarted = false;
|
|
3949
|
-
try {
|
|
3950
|
-
await processWebSocketStream(
|
|
3951
|
-
websocketUrl,
|
|
3952
|
-
websocketBody,
|
|
3953
|
-
websocketHeaders,
|
|
3954
|
-
output,
|
|
3955
|
-
stream,
|
|
3956
|
-
model,
|
|
3957
|
-
() => {
|
|
3958
|
-
websocketStarted = true;
|
|
3959
|
-
},
|
|
3960
|
-
options,
|
|
3961
|
-
deps,
|
|
3962
|
-
requestCwd,
|
|
3963
|
-
requestPrompt,
|
|
3964
|
-
webSearchCitationSources,
|
|
3965
|
-
historicalCitationSources,
|
|
3966
|
-
websocketRequestMetadata,
|
|
3967
|
-
settings.modelProfileHash,
|
|
3968
|
-
startupPrewarmTask,
|
|
3969
|
-
);
|
|
3970
|
-
if (options?.signal?.aborted) {
|
|
3971
|
-
throw new Error("Request was aborted");
|
|
3972
|
-
}
|
|
3973
|
-
finalizeUsage(model, output);
|
|
3974
|
-
stream.push({ type: "done", reason: output.stopReason as "stop" | "length" | "toolUse", message: output });
|
|
3975
|
-
stream.end();
|
|
3976
|
-
return;
|
|
3977
|
-
} catch (error) {
|
|
3978
|
-
const aborted = options?.signal?.aborted;
|
|
3979
|
-
const upgradeRejected = isWebSocketUpgradeRejectedError(error);
|
|
3980
|
-
if (
|
|
3981
|
-
transport === "auto"
|
|
3982
|
-
&& !websocketStarted
|
|
3983
|
-
&& upgradeRejected
|
|
3984
|
-
) {
|
|
3985
|
-
if (fallbackKey) websocketHttpFallbackSessions.add(fallbackKey);
|
|
3986
|
-
appendAssistantMessageDiagnostic(
|
|
3987
|
-
output,
|
|
3988
|
-
createAssistantMessageDiagnostic("provider_transport_failure", error, {
|
|
3989
|
-
configuredTransport,
|
|
3990
|
-
fallbackTransport: "sse",
|
|
3991
|
-
eventsEmitted: false,
|
|
3992
|
-
phase: "websocket_upgrade_rejected",
|
|
3993
|
-
retries: websocketRetries,
|
|
3994
|
-
requestBytes: new TextEncoder().encode(bodyJson).byteLength,
|
|
3995
|
-
}),
|
|
3996
|
-
);
|
|
3997
|
-
break;
|
|
3998
|
-
}
|
|
3999
|
-
const retryableTransport = !aborted
|
|
4000
|
-
&& (isWebSocketConnectionLimitReachedError(error) || isRetryableWebSocketError(error));
|
|
4001
|
-
const retryableBeforeStart = !websocketStarted && retryableTransport;
|
|
4002
|
-
if (retryableBeforeStart && websocketRetries < maxWebSocketRetries) {
|
|
4003
|
-
websocketRetries++;
|
|
4004
|
-
await sleep(webSocketRetryDelayMs(error, websocketRetries, options), options?.signal);
|
|
4005
|
-
continue;
|
|
4006
|
-
}
|
|
4007
|
-
if (aborted || (isProviderNonTransportError(error) && !isWebSocketConnectionLimitReachedError(error))) {
|
|
4008
|
-
throw error;
|
|
4009
|
-
}
|
|
4010
|
-
appendAssistantMessageDiagnostic(
|
|
4011
|
-
output,
|
|
4012
|
-
createAssistantMessageDiagnostic("provider_transport_failure", error, {
|
|
4013
|
-
configuredTransport,
|
|
4014
|
-
eventsEmitted: websocketStarted,
|
|
4015
|
-
phase: websocketStarted ? "after_message_stream_start" : "before_message_stream_start",
|
|
4016
|
-
retries: websocketRetries,
|
|
4017
|
-
requestBytes: new TextEncoder().encode(bodyJson).byteLength,
|
|
4018
|
-
}),
|
|
4019
|
-
);
|
|
4020
|
-
throw error;
|
|
4021
|
-
}
|
|
4022
|
-
}
|
|
4023
|
-
}
|
|
4024
|
-
|
|
4025
|
-
let response: Response | undefined;
|
|
4026
|
-
let lastError: Error | undefined;
|
|
4027
|
-
const sseUrl = resolveCodexUrl(model.baseUrl, { apiKeyMode: apiKeyTransport });
|
|
4028
|
-
const sseDispatcher = await proxyDispatcherForUrl(sseUrl);
|
|
4029
|
-
if (transformHeaders) {
|
|
4030
|
-
sseHeaders = providerHeadersToHeaders(
|
|
4031
|
-
await transformHeaders(headersToRecord(sseHeaders)),
|
|
4032
|
-
);
|
|
4033
|
-
}
|
|
4034
|
-
|
|
4035
|
-
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
4036
|
-
if (options?.signal?.aborted) {
|
|
4037
|
-
throw new Error("Request was aborted");
|
|
4038
|
-
}
|
|
4039
|
-
|
|
4040
|
-
try {
|
|
4041
|
-
response = await fetchWithResponseHeaderTimeout(sseUrl, {
|
|
4042
|
-
method: "POST",
|
|
4043
|
-
headers: sseHeaders,
|
|
4044
|
-
body: bodyJson,
|
|
4045
|
-
...(sseDispatcher ? { dispatcher: sseDispatcher } : {}),
|
|
4046
|
-
} as RequestInit, options?.signal, responseHeaderTimeoutMs);
|
|
4047
|
-
|
|
4048
|
-
await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
|
|
4049
|
-
|
|
4050
|
-
if (response.ok) {
|
|
4051
|
-
if (options?.sessionId) {
|
|
4052
|
-
captureCodexTurnState(
|
|
4053
|
-
options.sessionId,
|
|
4054
|
-
response.headers.get("x-codex-turn-state") ?? undefined,
|
|
4055
|
-
);
|
|
4056
|
-
}
|
|
4057
|
-
break;
|
|
4058
|
-
}
|
|
4059
|
-
|
|
4060
|
-
const errorText = await response.text();
|
|
4061
|
-
if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
|
|
4062
|
-
await sleep(BASE_DELAY_MS * 2 ** attempt, options?.signal);
|
|
4063
|
-
continue;
|
|
4064
|
-
}
|
|
4065
|
-
|
|
4066
|
-
const fakeResponse = new Response(errorText, {
|
|
4067
|
-
status: response.status,
|
|
4068
|
-
statusText: response.statusText,
|
|
4069
|
-
});
|
|
4070
|
-
const info = await parseErrorResponse(fakeResponse);
|
|
4071
|
-
throw new NonRetryableProviderError(withHttpStatusPrefix(response.status, info.friendlyMessage || info.message));
|
|
4072
|
-
} catch (error) {
|
|
4073
|
-
if (error instanceof NonRetryableProviderError) {
|
|
4074
|
-
throw error;
|
|
4075
|
-
}
|
|
4076
|
-
if (error instanceof Error && (error.name === "AbortError" || error.message === "Request was aborted")) {
|
|
4077
|
-
throw new Error("Request was aborted");
|
|
4078
|
-
}
|
|
4079
|
-
|
|
4080
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
4081
|
-
if (attempt < MAX_RETRIES && !lastError.message.includes("usage limit")) {
|
|
4082
|
-
await sleep(BASE_DELAY_MS * 2 ** attempt, options?.signal);
|
|
4083
|
-
continue;
|
|
4084
|
-
}
|
|
4085
|
-
throw lastError;
|
|
4086
|
-
}
|
|
4087
|
-
}
|
|
4088
|
-
|
|
4089
|
-
if (!response?.ok) {
|
|
4090
|
-
throw lastError ?? new Error("Failed after retries");
|
|
4091
|
-
}
|
|
4092
|
-
|
|
4093
|
-
if (!response.body) {
|
|
4094
|
-
throw new Error("No response body");
|
|
4095
|
-
}
|
|
4096
|
-
|
|
4097
|
-
stream.push({ type: "start", partial: output });
|
|
4098
|
-
await processCapturedResponsesStream(
|
|
4099
|
-
parseSSE(response),
|
|
4100
|
-
output,
|
|
4101
|
-
stream,
|
|
4102
|
-
model,
|
|
4103
|
-
options,
|
|
4104
|
-
options?.sessionId,
|
|
4105
|
-
deps,
|
|
4106
|
-
requestCwd,
|
|
4107
|
-
requestPrompt,
|
|
4108
|
-
webSearchCitationSources,
|
|
4109
|
-
historicalCitationSources,
|
|
4110
|
-
);
|
|
4111
|
-
finalizeUsage(model, output);
|
|
4112
|
-
|
|
4113
|
-
if (options?.signal?.aborted) {
|
|
4114
|
-
throw new Error("Request was aborted");
|
|
4115
|
-
}
|
|
4116
|
-
|
|
4117
|
-
stream.push({ type: "done", reason: output.stopReason as "stop" | "length" | "toolUse", message: output });
|
|
4118
|
-
stream.end();
|
|
4119
|
-
} catch (error) {
|
|
4120
|
-
stream.push({
|
|
4121
|
-
type: "error",
|
|
4122
|
-
reason: (options?.signal?.aborted ? "aborted" : "error") as "aborted" | "error",
|
|
4123
|
-
error: createErrorMessage(output, error, !!options?.signal?.aborted),
|
|
4124
|
-
});
|
|
4125
|
-
stream.end();
|
|
4126
|
-
}
|
|
4127
|
-
})();
|
|
4128
|
-
|
|
4129
|
-
return stream;
|
|
4130
|
-
}
|
|
4131
|
-
|
|
4132
|
-
export function registerOpenAIResponsesProviders(
|
|
4133
|
-
pi: ExtensionAPI,
|
|
4134
|
-
options: { getCurrentCwd: () => string },
|
|
4135
|
-
): OpenAIResponsesProviderController {
|
|
4136
|
-
installCodexIdentityLifecycle(pi);
|
|
4137
|
-
const pendingActivities: PendingActivity[] = [];
|
|
4138
|
-
const imagePreviewCache = new Map<string, CachedImagePreview>();
|
|
4139
|
-
const startupPrewarms = new Map<string, StartupPrewarmState>();
|
|
4140
|
-
const sessionStartupPrewarmTasks = new Map<string, SessionStartupPrewarmTask>();
|
|
4141
|
-
let sessionGeneration = 0;
|
|
4142
|
-
let pendingFlushTimer: ReturnType<typeof setTimeout> | undefined;
|
|
4143
|
-
|
|
4144
|
-
const abortStartupPrewarms = () => {
|
|
4145
|
-
for (const state of startupPrewarms.values()) state.abortController.abort();
|
|
4146
|
-
for (const task of sessionStartupPrewarmTasks.values()) task.abortController.abort();
|
|
4147
|
-
startupPrewarms.clear();
|
|
4148
|
-
sessionStartupPrewarmTasks.clear();
|
|
4149
|
-
};
|
|
4150
|
-
|
|
4151
|
-
const modelIdentity = (model: Model<Api>): string =>
|
|
4152
|
-
`${model.provider}\n${model.api}\n${model.id}\n${model.baseUrl}`;
|
|
4153
|
-
|
|
4154
|
-
const scheduleStartupPrewarm = (
|
|
4155
|
-
ctx: any,
|
|
4156
|
-
generation: number,
|
|
4157
|
-
startupSignal: AbortSignal,
|
|
4158
|
-
snapshot: StartupPrewarmSnapshot,
|
|
4159
|
-
): Promise<void> => (async () => {
|
|
4160
|
-
if (startupSignal.aborted) return;
|
|
4161
|
-
const model = ctx.model as Model<Api> | undefined;
|
|
4162
|
-
const sessionId = ctx?.sessionManager?.getSessionId?.();
|
|
4163
|
-
if (!model || !sessionId || generation !== sessionGeneration) return;
|
|
4164
|
-
const settings = loadModelSettings(model, ctx.cwd);
|
|
4165
|
-
if (
|
|
4166
|
-
!settings.enabled
|
|
4167
|
-
|| !settings.modelProfile?.effective.enabled
|
|
4168
|
-
|| !settings.providerShimActive
|
|
4169
|
-
|| !settings.openaiWebSocketPrewarm
|
|
4170
|
-
|| settings.openaiTransport === "sse"
|
|
4171
|
-
) {
|
|
4172
|
-
return;
|
|
4173
|
-
}
|
|
4174
|
-
|
|
4175
|
-
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
4176
|
-
if (
|
|
4177
|
-
startupSignal.aborted
|
|
4178
|
-
|| generation !== sessionGeneration
|
|
4179
|
-
|| !auth.ok
|
|
4180
|
-
|| !hasCodexRequestAuth({
|
|
4181
|
-
modelHeaders: model.headers,
|
|
4182
|
-
auth: { apiKey: auth.apiKey, headers: auth.headers },
|
|
4183
|
-
})
|
|
4184
|
-
) {
|
|
4185
|
-
return;
|
|
4186
|
-
}
|
|
4187
|
-
|
|
4188
|
-
const profile = resolveCodexRequestProfile(settings.requestProfile);
|
|
4189
|
-
const requestIdentity = resolveCodexRequestIdentity(
|
|
4190
|
-
sessionId,
|
|
4191
|
-
undefined,
|
|
4192
|
-
"prewarm",
|
|
4193
|
-
);
|
|
4194
|
-
// Match Codex startup prewarm: snapshot only stable request context.
|
|
4195
|
-
// Conversation history and the first user message are sent by the first
|
|
4196
|
-
// real request as an incremental continuation from this response.
|
|
4197
|
-
let body = applyFastModeServiceTier(
|
|
4198
|
-
buildRequestBody(model, {
|
|
4199
|
-
systemPrompt: snapshot.systemPrompt,
|
|
4200
|
-
messages: [],
|
|
4201
|
-
tools: snapshot.tools,
|
|
4202
|
-
}, profile, {
|
|
4203
|
-
apiKey: auth.apiKey,
|
|
4204
|
-
headers: auth.headers,
|
|
4205
|
-
sessionId,
|
|
4206
|
-
reasoning: snapshot.reasoning ?? (model.reasoning ? "medium" : undefined),
|
|
4207
|
-
}),
|
|
4208
|
-
settings,
|
|
4209
|
-
model,
|
|
4210
|
-
);
|
|
4211
|
-
if (settings.nativeProviderTools) {
|
|
4212
|
-
const webSearch = settings.modelProfile.effective.tools.webSearch;
|
|
4213
|
-
body = rewriteNativeOpenAiTools(body, {
|
|
4214
|
-
imageModel: settings.imageModel,
|
|
4215
|
-
imageGeneration: settings.imageGenerationImplementation ?? false,
|
|
4216
|
-
webSearch: settings.webSearchEnabled && webSearch
|
|
4217
|
-
? {
|
|
4218
|
-
implementation: webSearch.implementation,
|
|
4219
|
-
contentTypes: webSearch.contentTypes,
|
|
4220
|
-
}
|
|
4221
|
-
: false,
|
|
4222
|
-
}).payload;
|
|
4223
|
-
}
|
|
4224
|
-
ensureWebSearchDetailsIncluded(body);
|
|
4225
|
-
body = withResponsesLiteWebSocketMetadata(body, profile.responsesMode);
|
|
4226
|
-
|
|
4227
|
-
const websocketUrl = resolveResponsesWebSocketUrl(model.baseUrl, { apiKeyMode: settings.apiKeyMode });
|
|
4228
|
-
const fallbackKey = webSocketFallbackKey(
|
|
4229
|
-
sessionId,
|
|
4230
|
-
model,
|
|
4231
|
-
websocketUrl,
|
|
4232
|
-
settings.modelProfileHash,
|
|
4233
|
-
);
|
|
4234
|
-
if (settings.openaiTransport === "auto" && fallbackKey && websocketHttpFallbackSessions.has(fallbackKey)) {
|
|
4235
|
-
return;
|
|
4236
|
-
}
|
|
4237
|
-
if (startupSignal.aborted || generation !== sessionGeneration) return;
|
|
4238
|
-
const headers = applyConfiguredResponsesFeatureHeaders(buildWebSocketHeaders(
|
|
4239
|
-
model.headers,
|
|
4240
|
-
auth.headers,
|
|
4241
|
-
resolveCodexRequestAccountId({
|
|
4242
|
-
modelHeaders: model.headers,
|
|
4243
|
-
auth: { apiKey: auth.apiKey, headers: auth.headers },
|
|
4244
|
-
apiKeyMode: settings.apiKeyMode,
|
|
4245
|
-
}),
|
|
4246
|
-
auth.apiKey ?? "",
|
|
4247
|
-
sessionId,
|
|
4248
|
-
requestIdentity?.threadId ?? sessionId,
|
|
4249
|
-
requestIdentity,
|
|
4250
|
-
), settings, model);
|
|
4251
|
-
const cacheKey = webSocketCacheKey(
|
|
4252
|
-
sessionId,
|
|
4253
|
-
model,
|
|
4254
|
-
websocketUrl,
|
|
4255
|
-
headers,
|
|
4256
|
-
settings.modelProfileHash,
|
|
4257
|
-
);
|
|
4258
|
-
if (
|
|
4259
|
-
!cacheKey
|
|
4260
|
-
|| startupSignal.aborted
|
|
4261
|
-
|| generation !== sessionGeneration
|
|
4262
|
-
|| startupPrewarms.has(cacheKey)
|
|
4263
|
-
) {
|
|
4264
|
-
return;
|
|
4265
|
-
}
|
|
4266
|
-
if (websocketSessionCache.get(cacheKey)?.continuation) return;
|
|
4267
|
-
|
|
4268
|
-
const abortController = new AbortController();
|
|
4269
|
-
const abortFromSession = () => abortController.abort();
|
|
4270
|
-
startupSignal.addEventListener("abort", abortFromSession, { once: true });
|
|
4271
|
-
const state: StartupPrewarmState = {
|
|
4272
|
-
status: "pending",
|
|
4273
|
-
abortController,
|
|
4274
|
-
promise: Promise.resolve(),
|
|
4275
|
-
};
|
|
4276
|
-
startupPrewarms.set(cacheKey, state);
|
|
4277
|
-
const timeout = setTimeout(() => abortController.abort(), WEBSOCKET_PREWARM_TIMEOUT_MS);
|
|
4278
|
-
state.promise = prewarmWebSocket({
|
|
4279
|
-
url: websocketUrl,
|
|
4280
|
-
headers,
|
|
4281
|
-
cacheKey,
|
|
4282
|
-
body,
|
|
4283
|
-
requestMetadata: {
|
|
4284
|
-
sessionId,
|
|
4285
|
-
threadId: requestIdentity?.threadId ?? sessionId,
|
|
4286
|
-
turnId: requestIdentity?.turnId ?? "",
|
|
4287
|
-
requestKind: "prewarm",
|
|
4288
|
-
...(requestIdentity ? { identity: requestIdentity } : {}),
|
|
4289
|
-
},
|
|
4290
|
-
signal: abortController.signal,
|
|
4291
|
-
connectTimeoutMs: WEBSOCKET_PREWARM_TIMEOUT_MS,
|
|
4292
|
-
})
|
|
4293
|
-
.then(() => {
|
|
4294
|
-
state.status = "ready";
|
|
4295
|
-
})
|
|
4296
|
-
.catch((error) => {
|
|
4297
|
-
state.status = "failed";
|
|
4298
|
-
if (
|
|
4299
|
-
settings.openaiTransport === "auto"
|
|
4300
|
-
&& fallbackKey
|
|
4301
|
-
&& isWebSocketUpgradeRejectedError(error)
|
|
4302
|
-
) {
|
|
4303
|
-
websocketHttpFallbackSessions.add(fallbackKey);
|
|
4304
|
-
}
|
|
4305
|
-
})
|
|
4306
|
-
.finally(() => {
|
|
4307
|
-
clearTimeout(timeout);
|
|
4308
|
-
startupSignal.removeEventListener("abort", abortFromSession);
|
|
4309
|
-
});
|
|
4310
|
-
await state.promise;
|
|
4311
|
-
})();
|
|
4312
|
-
|
|
4313
|
-
const flushPendingMessages = () => {
|
|
4314
|
-
pendingFlushTimer = undefined;
|
|
4315
|
-
const activities = pendingActivities.splice(0, pendingActivities.length);
|
|
4316
|
-
|
|
4317
|
-
for (const activity of activities) {
|
|
4318
|
-
imagePreviewCache.set(activity.savedImage.absolutePath, makeCachedImagePreview(activity.imageData.data, activity.imageData.mimeType));
|
|
4319
|
-
pi.sendMessage(
|
|
4320
|
-
{
|
|
4321
|
-
customType: IMAGE_SAVE_DISPLAY_MESSAGE_TYPE,
|
|
4322
|
-
content: [{ type: "text", text: buildGeneratedImageDisplayText(activity.savedImage, { expanded: false }) }],
|
|
4323
|
-
display: true,
|
|
4324
|
-
details: { savedImages: [activity.savedImage] } satisfies ImageDisplayMessageDetails,
|
|
4325
|
-
},
|
|
4326
|
-
{ triggerTurn: false },
|
|
4327
|
-
);
|
|
4328
|
-
}
|
|
4329
|
-
};
|
|
4330
|
-
|
|
4331
|
-
const schedulePendingMessageFlush = () => {
|
|
4332
|
-
if (pendingFlushTimer || pendingActivities.length === 0) {
|
|
4333
|
-
return;
|
|
4334
|
-
}
|
|
4335
|
-
pendingFlushTimer = setTimeout(flushPendingMessages, 0);
|
|
4336
|
-
};
|
|
4337
|
-
|
|
4338
|
-
const clearPendingMessages = () => {
|
|
4339
|
-
if (pendingFlushTimer) {
|
|
4340
|
-
clearTimeout(pendingFlushTimer);
|
|
4341
|
-
pendingFlushTimer = undefined;
|
|
4342
|
-
}
|
|
4343
|
-
pendingActivities.length = 0;
|
|
4344
|
-
imagePreviewCache.clear();
|
|
4345
|
-
};
|
|
4346
|
-
|
|
4347
|
-
const streamSimple = <TApi extends Api>(model: Model<TApi>, context: Context, streamOptions?: SimpleStreamOptions) => {
|
|
4348
|
-
const settings = loadModelSettings(model, options.getCurrentCwd());
|
|
4349
|
-
if (
|
|
4350
|
-
!settings.enabled
|
|
4351
|
-
|| !settings.modelProfile?.effective.enabled
|
|
4352
|
-
|| !settings.providerShimActive
|
|
4353
|
-
) {
|
|
4354
|
-
return model.api === "openai-codex-responses"
|
|
4355
|
-
? streamSimpleOpenAICodexResponses(model as Model<"openai-codex-responses">, context, streamOptions)
|
|
4356
|
-
: streamSimpleOpenAIResponses(model as Model<"openai-responses">, context, streamOptions);
|
|
4357
|
-
}
|
|
4358
|
-
return createCodexStream(model, context, streamOptions, {
|
|
4359
|
-
getCurrentCwd: options.getCurrentCwd,
|
|
4360
|
-
getCurrentTurnId: (sessionId) =>
|
|
4361
|
-
currentCodexTurn(sessionId)?.turnId,
|
|
4362
|
-
getStartupPrewarm: (sessionId, requestModel) => {
|
|
4363
|
-
const task = sessionStartupPrewarmTasks.get(sessionId);
|
|
4364
|
-
return task
|
|
4365
|
-
&& task.generation === sessionGeneration
|
|
4366
|
-
&& task.modelIdentity === modelIdentity(requestModel)
|
|
4367
|
-
? task.promise
|
|
4368
|
-
: undefined;
|
|
4369
|
-
},
|
|
4370
|
-
onImageSaved: (savedImage, imageData) => {
|
|
4371
|
-
pendingActivities.push({ kind: "image", savedImage, imageData });
|
|
4372
|
-
},
|
|
4373
|
-
});
|
|
4374
|
-
};
|
|
4375
|
-
|
|
4376
|
-
type CodexResponsesApi = "openai-responses" | "openai-codex-responses";
|
|
4377
|
-
const registeredProviderApis = new Map<string, CodexResponsesApi>();
|
|
4378
|
-
const registerProviderShim = (provider: string, api: CodexResponsesApi): void => {
|
|
4379
|
-
if (!provider || registeredProviderApis.get(provider) === api) return;
|
|
4380
|
-
pi.registerProvider(provider, { api, streamSimple });
|
|
4381
|
-
registeredProviderApis.set(provider, api);
|
|
4382
|
-
};
|
|
4383
|
-
const ensureProviderShimForModel = (model: Model<Api> | undefined, cwd?: string): void => {
|
|
4384
|
-
if (!model) return;
|
|
4385
|
-
const settings = loadModelSettings(model, cwd);
|
|
4386
|
-
if (
|
|
4387
|
-
!settings.enabled
|
|
4388
|
-
|| !settings.modelProfile?.effective.enabled
|
|
4389
|
-
|| !settings.providerShimActive
|
|
4390
|
-
) {
|
|
4391
|
-
return;
|
|
4392
|
-
}
|
|
4393
|
-
if (model.api === "openai-responses" || model.api === "openai-codex-responses") {
|
|
4394
|
-
registerProviderShim(model.provider, model.api as CodexResponsesApi);
|
|
4395
|
-
}
|
|
4396
|
-
};
|
|
4397
|
-
|
|
4398
|
-
// Pi 0.75 dispatches extension streams by API type, while newer Pi versions
|
|
4399
|
-
// compose them per provider. Register both built-ins first. A user-defined
|
|
4400
|
-
// provider is registered only after Pi supplies an actual selected model, so
|
|
4401
|
-
// this extension never creates or overwrites its URL, auth, or model list.
|
|
4402
|
-
registerProviderShim("openai-codex", "openai-codex-responses");
|
|
4403
|
-
registerProviderShim("openai", "openai-responses");
|
|
4404
|
-
|
|
4405
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
4406
|
-
sessionGeneration++;
|
|
4407
|
-
abortStartupPrewarms();
|
|
4408
|
-
ensureProviderShimForModel(ctx?.model as Model<Api> | undefined, ctx?.cwd);
|
|
4409
|
-
clearPendingMessages();
|
|
4410
|
-
const generation = sessionGeneration;
|
|
4411
|
-
// Do not block session startup. The first provider request naturally
|
|
4412
|
-
// serializes behind this socket operation if it is still pending.
|
|
4413
|
-
const model = ctx?.model as Model<Api> | undefined;
|
|
4414
|
-
const sessionId = ctx?.sessionManager?.getSessionId?.();
|
|
4415
|
-
if (model && sessionId) {
|
|
4416
|
-
const abortController = new AbortController();
|
|
4417
|
-
const snapshot = startupPrewarmSnapshot(pi, ctx);
|
|
4418
|
-
const promise = scheduleStartupPrewarm(ctx, generation, abortController.signal, snapshot);
|
|
4419
|
-
sessionStartupPrewarmTasks.set(sessionId, {
|
|
4420
|
-
generation,
|
|
4421
|
-
modelIdentity: modelIdentity(model),
|
|
4422
|
-
promise,
|
|
4423
|
-
abortController,
|
|
4424
|
-
});
|
|
4425
|
-
void promise;
|
|
4426
|
-
}
|
|
4427
|
-
});
|
|
4428
|
-
|
|
4429
|
-
pi.on("model_select", async (_event, ctx) => {
|
|
4430
|
-
ensureProviderShimForModel(ctx?.model as Model<Api> | undefined, ctx?.cwd);
|
|
4431
|
-
});
|
|
4432
|
-
|
|
4433
|
-
pi.on("session_shutdown", async (_event, ctx) => {
|
|
4434
|
-
sessionGeneration++;
|
|
4435
|
-
abortStartupPrewarms();
|
|
4436
|
-
if (pendingActivities.length > 0) {
|
|
4437
|
-
flushPendingMessages();
|
|
4438
|
-
}
|
|
4439
|
-
closeProviderWebSocketSessions(
|
|
4440
|
-
ctx?.sessionManager?.getSessionId?.(),
|
|
4441
|
-
);
|
|
4442
|
-
clearPendingMessages();
|
|
4443
|
-
});
|
|
4444
|
-
|
|
4445
|
-
pi.on("before_agent_start", async (_event, ctx) => {
|
|
4446
|
-
ensureProviderShimForModel(ctx.model as Model<Api> | undefined, ctx.cwd);
|
|
4447
|
-
});
|
|
4448
|
-
|
|
4449
|
-
pi.on("agent_end", async (_event, ctx) => {
|
|
4450
|
-
schedulePendingMessageFlush();
|
|
4451
|
-
});
|
|
4452
|
-
|
|
4453
|
-
pi.registerMessageRenderer<ImageDisplayMessageDetails>(IMAGE_SAVE_DISPLAY_MESSAGE_TYPE, (message, options, theme) => {
|
|
4454
|
-
const savedImage = message.details?.savedImages?.[0];
|
|
4455
|
-
const textContent = typeof message.content === "string"
|
|
4456
|
-
? message.content
|
|
4457
|
-
: message.content
|
|
4458
|
-
.filter((item) => item.type === "text")
|
|
4459
|
-
.map((item) => item.text)
|
|
4460
|
-
.join("\n");
|
|
4461
|
-
return renderImageGenerationMessage(savedImage, textContent, options, theme, imagePreviewCache);
|
|
4462
|
-
});
|
|
4463
|
-
|
|
4464
|
-
pi.registerMessageRenderer<{ searches?: SurfacedWebSearch[] }>(WEB_SEARCH_ACTIVITY_MESSAGE_TYPE, (message, options, theme) => {
|
|
4465
|
-
const searches = message.details?.searches ?? [];
|
|
4466
|
-
const container = new Container();
|
|
4467
|
-
if (searches.length > 0) {
|
|
4468
|
-
searches.forEach((search, index) => {
|
|
4469
|
-
const completed = search.completed ?? search.status === "completed";
|
|
4470
|
-
const header = completed ? "Searched the web" : "Searching the web";
|
|
4471
|
-
const detail = webSearchActivityDetail(search);
|
|
4472
|
-
const separator = detail ? (completed ? " for " : " ") : "";
|
|
4473
|
-
const bullet = themeFg(theme, completed ? "muted" : "accent", glyphs().bullet);
|
|
4474
|
-
const lines = [
|
|
4475
|
-
`${bullet}${themeFg(theme, "text", themeBold(theme, header))}${themeFg(theme, "dim", `${separator}${detail}`)}`,
|
|
4476
|
-
];
|
|
4477
|
-
const hosts = webSearchActivityHosts(search);
|
|
4478
|
-
if (hosts.length > 0) {
|
|
4479
|
-
const shown = hosts.slice(0, 8);
|
|
4480
|
-
const hostLine = shown.map((host) => themeFg(theme, "accent", host));
|
|
4481
|
-
if (hosts.length > shown.length) {
|
|
4482
|
-
hostLine.push(themeFg(theme, "dim", `+${hosts.length - shown.length}`));
|
|
4483
|
-
}
|
|
4484
|
-
lines.push(` ${hostLine.join(themeFg(theme, "dim", glyphs().dot))}`);
|
|
4485
|
-
}
|
|
4486
|
-
container.addChild(new Text(`${index > 0 ? "\n" : ""}${lines.join("\n")}`, 0, 0));
|
|
4487
|
-
});
|
|
4488
|
-
} else {
|
|
4489
|
-
container.addChild(new Text(themeFg(theme, "text", themeBold(theme, buildWebSearchSummaryText(searches))), 0, 0));
|
|
4490
|
-
}
|
|
4491
|
-
if (options.expanded) {
|
|
4492
|
-
const content = typeof message.content === "string"
|
|
4493
|
-
? message.content
|
|
4494
|
-
: message.content
|
|
4495
|
-
.filter((item) => item.type === "text")
|
|
4496
|
-
.map((item) => item.text)
|
|
4497
|
-
.join("\n");
|
|
4498
|
-
container.addChild(new Text(`\n${themeFg(theme, "dim", content)}`, 0, 0));
|
|
4499
|
-
}
|
|
4500
|
-
return container;
|
|
4501
|
-
});
|
|
4502
|
-
|
|
4503
|
-
return {
|
|
4504
|
-
getCurrentTurnId(sessionId) {
|
|
4505
|
-
return currentCodexTurn(sessionId)?.turnId;
|
|
4506
|
-
},
|
|
4507
|
-
getRequestIdentity(sessionId, requestKind = "turn") {
|
|
4508
|
-
return resolveCodexRequestIdentity(sessionId, undefined, requestKind);
|
|
4509
|
-
},
|
|
4510
|
-
};
|
|
4511
|
-
}
|
|
1
|
+
// Compatibility facade. Implementations must import their owning modules directly.
|
|
2
|
+
export { buildCodexCompactionCheckpoint, sanitizeNativeCompactionOutput } from "@oai404iao/pi-codex-core/internal/adapter/compaction/checkpoint";
|
|
3
|
+
export { requestOpenAINativeCompaction } from "@oai404iao/pi-codex-core/internal/adapter/compaction/request";
|
|
4
|
+
export { registerOpenAIResponsesProviders } from "./extension/register.js";
|
|
5
|
+
export { buildProviderErrorMessage, withHttpStatusPrefix } from "@oai404iao/pi-codex-core/internal/providers/openai-codex/errors";
|
|
6
|
+
export { buildSSEHeaders, buildWebSocketHeaders } from "@oai404iao/pi-codex-core/internal/providers/openai-codex/headers";
|
|
7
|
+
export { withResponsesLiteWebSocketMetadata } from "@oai404iao/pi-codex-core/internal/providers/openai-codex/lite";
|
|
8
|
+
export { proxyForWebSocketUrl, webSocketOptionsForUrl } from "@oai404iao/pi-codex-core/internal/providers/openai-codex/proxy";
|
|
9
|
+
export { buildRequestBody } from "@oai404iao/pi-codex-core/internal/providers/openai-codex/request-body";
|
|
10
|
+
export { withSseRequestMetadata } from "@oai404iao/pi-codex-core/internal/providers/openai-codex/request-metadata";
|
|
11
|
+
export { fetchWithResponseHeaderTimeout, responseHeaderTimeoutMsFromOptions } from "@oai404iao/pi-codex-core/internal/providers/openai-codex/sse";
|
|
12
|
+
export type { OpenAIResponsesProviderController } from "@oai404iao/pi-codex-runtime/internal/providers/openai-codex/types";
|
|
13
|
+
export { resolveCodexUrl, resolveResponsesWebSocketUrl } from "@oai404iao/pi-codex-core/internal/providers/openai-codex/urls";
|
|
14
|
+
export { sendWebSocketRequest } from "@oai404iao/pi-codex-core/internal/providers/openai-codex/websocket-events";
|
|
15
|
+
export { closeProviderWebSocketSessions } from "@oai404iao/pi-codex-core/internal/providers/openai-codex/websocket-session";
|
|
16
|
+
export { buildGeneratedImageDisplayText, getOpenAICodexImageDirectory, getOpenAICodexImagePath, getOpenAICodexLatestImagePath, saveOpenAICodexGeneratedImage } from "@oai404iao/pi-codex-imagegen/internal/tools/image-generation/storage";
|
|
17
|
+
export { IMAGE_SAVE_DISPLAY_MESSAGE_TYPE } from "@oai404iao/pi-codex-imagegen/internal/tools/image-generation/types";
|
|
18
|
+
export type { SavedGeneratedImage } from "@oai404iao/pi-codex-imagegen/internal/tools/image-generation/types";
|
|
19
|
+
export { WEB_SEARCH_ACTIVITY_MESSAGE_TYPE, buildWebSearchActivityMessage, buildWebSearchInlineText, buildWebSearchStatusText, buildWebSearchSummaryText, extractWebSearch, extractWebSearchProgress, mergeWebSearchActivity, webSearchActivityDetail, webSearchActivityHosts } from "@oai404iao/pi-codex-web-search/internal/tools/web-search/activity";
|
|
20
|
+
export type { SurfacedWebSearch } from "@oai404iao/pi-codex-web-search/internal/tools/web-search/activity";
|