@gajae-code/ai 0.18.1 → 0.18.6
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/CHANGELOG.md +74 -0
- package/dist/types/auth-storage.d.ts +8 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/providers/cursor.d.ts +6 -3
- package/dist/types/providers/kiro-api-key.d.ts +1 -0
- package/dist/types/providers/openai-codex-responses.d.ts +2 -0
- package/dist/types/types.d.ts +1 -1
- package/dist/types/utils/fallback-transport.d.ts +6 -0
- package/dist/types/utils/oauth/glm-zcode.d.ts +5 -0
- package/dist/types/utils/oauth/index.d.ts +4 -0
- package/package.json +3 -3
- package/src/auth-storage.ts +115 -39
- package/src/index.ts +0 -1
- package/src/model-pricing.ts +10 -0
- package/src/model-thinking.ts +8 -0
- package/src/models.json +210 -0
- package/src/providers/anthropic.ts +25 -1
- package/src/providers/aws-eventstream.ts +2 -1
- package/src/providers/cursor.d.ts +6 -3
- package/src/providers/cursor.ts +73 -64
- package/src/providers/kiro-api-key.d.ts +1 -0
- package/src/providers/kiro-api-key.ts +1 -1
- package/src/providers/kiro-codewhisperer.ts +142 -34
- package/src/providers/openai-codex-responses.d.ts +2 -0
- package/src/providers/openai-codex-responses.ts +438 -40
- package/src/providers/openai-completions.ts +19 -1
- package/src/stream.ts +20 -0
- package/src/types.d.ts +1 -1
- package/src/types.ts +3 -0
- package/src/utils/fallback-transport.d.ts +6 -0
- package/src/utils/fallback-transport.ts +6 -0
- package/src/utils/idle-iterator.ts +10 -3
- package/src/utils/oauth/glm-zcode.d.ts +5 -0
- package/src/utils/oauth/glm-zcode.ts +8 -1
- package/src/utils/oauth/index.ts +11 -1
- package/src/utils/validation.ts +5 -0
- package/dist/types/utils/h2-fetch.d.ts +0 -35
- package/src/utils/h2-fetch.ts +0 -133
|
@@ -52,7 +52,11 @@ import {
|
|
|
52
52
|
sanitizeOpenAIResponsesHistoryItemsForReplay,
|
|
53
53
|
} from "../utils";
|
|
54
54
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
55
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
SERVER_OVERLOADED_PROVIDER_CODE,
|
|
57
|
+
STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE,
|
|
58
|
+
transportFailureFacts,
|
|
59
|
+
} from "../utils/fallback-transport";
|
|
56
60
|
import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
|
|
57
61
|
import {
|
|
58
62
|
getOpenAIStreamIdleTimeoutMs,
|
|
@@ -114,8 +118,12 @@ const CODEX_MAX_RETRIES = 5;
|
|
|
114
118
|
const CODEX_RETRY_DELAY_MS = 500;
|
|
115
119
|
const CODEX_WEBSOCKET_CONNECT_TIMEOUT_MS = 10000;
|
|
116
120
|
const CODEX_WEBSOCKET_IDLE_TIMEOUT_MS = 300000;
|
|
121
|
+
/** Longest delay setTimeout honors; larger delays fire after 1 ms, so waits are capped here. */
|
|
122
|
+
const CODEX_MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
117
123
|
const CODEX_WEBSOCKET_RETRY_BUDGET = CODEX_MAX_RETRIES;
|
|
118
124
|
const CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX = "Codex websocket transport error";
|
|
125
|
+
const CODEX_SALVAGED_STREAM_CLOSE_ERROR_CODE = "codex_stream_closed_after_finalized_tool_calls";
|
|
126
|
+
const CODEX_SALVAGED_COMPLETE_TOOL_ARGUMENTS_ERROR_CODE = "codex_stream_closed_after_complete_tool_arguments";
|
|
119
127
|
const CODEX_PREVIOUS_RESPONSE_STALE_CODES = new Set(["previous_response_not_found", "codex_previous_response_stale"]);
|
|
120
128
|
// Some Codex deployments reject a stale continuation anchor with a generic
|
|
121
129
|
// `invalid_request_error` code and name the anchor only in the message
|
|
@@ -157,7 +165,17 @@ const CODEX_PREVIOUS_RESPONSE_STALE_PROSE_MESSAGE = new RegExp(
|
|
|
157
165
|
`|${CODEX_PREVIOUS_RESPONSE_PROSE_TOKEN}(?:(?!${CODEX_PREVIOUS_RESPONSE_STALE_SUBFIELD_GUARD})[^\\n]){0,48}?(?:${CODEX_ANCHOR_STALE_QUALIFIER})(?![^\\n]{0,48}(?:${CODEX_PREVIOUS_RESPONSE_STALE_SUBFIELD_GUARD}))`,
|
|
158
166
|
"i",
|
|
159
167
|
);
|
|
160
|
-
const CODEX_RETRYABLE_EVENT_CODES = new Set([
|
|
168
|
+
const CODEX_RETRYABLE_EVENT_CODES = new Set([
|
|
169
|
+
"model_error",
|
|
170
|
+
"server_error",
|
|
171
|
+
"internal_error",
|
|
172
|
+
SERVER_OVERLOADED_PROVIDER_CODE,
|
|
173
|
+
]);
|
|
174
|
+
const CODEX_TYPED_TRANSPORT_PROVIDER_CODES = new Set([
|
|
175
|
+
SERVER_OVERLOADED_PROVIDER_CODE,
|
|
176
|
+
"server_error",
|
|
177
|
+
"internal_error",
|
|
178
|
+
]);
|
|
161
179
|
const CODEX_NON_RETRYABLE_EVENT_CODES = new Set([
|
|
162
180
|
"invalid_function_parameters",
|
|
163
181
|
"invalid_request_error",
|
|
@@ -176,11 +194,31 @@ const CODEX_RETRYABLE_EVENT_MESSAGE =
|
|
|
176
194
|
/processing your request|retry your request|temporar(?:y|ily)|overloaded|service.?unavailable|internal error|server error/i;
|
|
177
195
|
const CODEX_ACCOUNT_MODEL_UNAVAILABLE_MESSAGE = /\bnot supported when using codex with a chatgpt account\b/i;
|
|
178
196
|
const CODEX_PROVIDER_SESSION_STATE_KEY = "openai-codex-responses";
|
|
197
|
+
const providerSessionStateIdentities = new WeakMap<Map<string, ProviderSessionState>, number>();
|
|
198
|
+
const disabledCodexPublicSessionKeys = new Set<string>();
|
|
199
|
+
let nextProviderSessionStateIdentity = 1;
|
|
200
|
+
|
|
201
|
+
function getProviderSessionStateIdentity(
|
|
202
|
+
providerSessionState: Map<string, ProviderSessionState> | undefined,
|
|
203
|
+
): number | undefined {
|
|
204
|
+
if (!providerSessionState) return undefined;
|
|
205
|
+
let identity = providerSessionStateIdentities.get(providerSessionState);
|
|
206
|
+
if (identity === undefined) {
|
|
207
|
+
identity = nextProviderSessionStateIdentity++;
|
|
208
|
+
providerSessionStateIdentities.set(providerSessionState, identity);
|
|
209
|
+
}
|
|
210
|
+
return identity;
|
|
211
|
+
}
|
|
179
212
|
const X_CODEX_TURN_STATE_HEADER = "x-codex-turn-state";
|
|
180
213
|
const X_MODELS_ETAG_HEADER = "x-models-etag";
|
|
181
214
|
const X_REASONING_INCLUDED_HEADER = "x-reasoning-included";
|
|
182
215
|
/** Connection-level websocket failures that should immediately fall back to SSE without retrying. */
|
|
183
|
-
const CODEX_WEBSOCKET_FATAL_PATTERNS = [
|
|
216
|
+
const CODEX_WEBSOCKET_FATAL_PATTERNS = [
|
|
217
|
+
"websocket error:",
|
|
218
|
+
"websocket closed before open",
|
|
219
|
+
"websocket is closed before the connection is established",
|
|
220
|
+
"connection timeout",
|
|
221
|
+
];
|
|
184
222
|
/** Max total time to spend retrying 429s with server-provided delays (5 minutes). */
|
|
185
223
|
const CODEX_RATE_LIMIT_BUDGET_MS = 5 * 60 * 1000;
|
|
186
224
|
|
|
@@ -207,10 +245,7 @@ const CODEX_PROGRESS_EVENT_TYPES = new Set([
|
|
|
207
245
|
]);
|
|
208
246
|
|
|
209
247
|
/**
|
|
210
|
-
* A progress event must carry
|
|
211
|
-
* predicate: a recognized envelope whose `delta` is absent or not a non-empty
|
|
212
|
-
* string is NOT progress — otherwise repeated malformed/no-op deltas reset the
|
|
213
|
-
* idle watchdog indefinitely and a managed attempt need never terminate.
|
|
248
|
+
* A progress event must carry a non-empty delta for delta events.
|
|
214
249
|
* Non-delta envelope types (lifecycle, item boundaries, terminal events) count
|
|
215
250
|
* as progress by type alone, as before.
|
|
216
251
|
*/
|
|
@@ -223,13 +258,100 @@ function isCodexStreamProgressEvent(event: unknown): boolean {
|
|
|
223
258
|
return typeof delta === "string" && delta.length > 0;
|
|
224
259
|
}
|
|
225
260
|
|
|
261
|
+
type CodexArgumentBuffer = { value: string };
|
|
262
|
+
|
|
263
|
+
/** @internal Exported for tests. */
|
|
264
|
+
export function createCodexStreamProgressClassifier(): (event: unknown) => boolean {
|
|
265
|
+
const argumentBuffers = new Map<string, CodexArgumentBuffer>();
|
|
266
|
+
|
|
267
|
+
const getArgumentKey = (event: Record<string, unknown>): string | undefined => {
|
|
268
|
+
if (typeof event.item_id === "string") return `item:${event.item_id}`;
|
|
269
|
+
if (typeof event.output_index === "number") return `output:${event.output_index}`;
|
|
270
|
+
return undefined;
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const rememberFunctionCallItem = (event: Record<string, unknown>): void => {
|
|
274
|
+
if (event.type !== "response.output_item.added" || !event.item || typeof event.item !== "object") return;
|
|
275
|
+
const item = event.item as Record<string, unknown>;
|
|
276
|
+
if (item.type !== "function_call" || typeof item.arguments !== "string") return;
|
|
277
|
+
const buffer = { value: item.arguments };
|
|
278
|
+
if (typeof item.id === "string") argumentBuffers.set(`item:${item.id}`, buffer);
|
|
279
|
+
if (typeof event.output_index === "number") argumentBuffers.set(`output:${event.output_index}`, buffer);
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// Reads `type` and `delta` exactly once per event: the stream handler reads the
|
|
283
|
+
// same parsed event afterwards, and a second read here could observe a
|
|
284
|
+
// different value than the one that is classified and assembled.
|
|
285
|
+
return event => {
|
|
286
|
+
if (!event || typeof event !== "object") return false;
|
|
287
|
+
const record = event as Record<string, unknown>;
|
|
288
|
+
const type = record.type;
|
|
289
|
+
if (typeof type !== "string" || !CODEX_PROGRESS_EVENT_TYPES.has(type)) return false;
|
|
290
|
+
if (type.endsWith(".delta")) {
|
|
291
|
+
const delta = record.delta;
|
|
292
|
+
if (typeof delta !== "string" || delta.length === 0) return false;
|
|
293
|
+
if (type !== "response.function_call_arguments.delta") return true;
|
|
294
|
+
return classifyArgumentDelta(record, delta);
|
|
295
|
+
}
|
|
296
|
+
rememberFunctionCallItem(record);
|
|
297
|
+
if (type === "response.function_call_arguments.done") {
|
|
298
|
+
const key = getArgumentKey(record);
|
|
299
|
+
const buffer = key ? argumentBuffers.get(key) : undefined;
|
|
300
|
+
const argumentsValue = record.arguments;
|
|
301
|
+
if (buffer && typeof argumentsValue === "string") buffer.value = argumentsValue;
|
|
302
|
+
}
|
|
303
|
+
return true;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
function classifyArgumentDelta(record: Record<string, unknown>, delta: string): boolean {
|
|
307
|
+
const key = getArgumentKey(record);
|
|
308
|
+
if (!key) return true;
|
|
309
|
+
let buffer = argumentBuffers.get(key);
|
|
310
|
+
if (!buffer) {
|
|
311
|
+
buffer = { value: "" };
|
|
312
|
+
argumentBuffers.set(key, buffer);
|
|
313
|
+
}
|
|
314
|
+
const isWhitespaceOnly = delta.trim().length === 0;
|
|
315
|
+
const wasComplete =
|
|
316
|
+
isWhitespaceOnly &&
|
|
317
|
+
(() => {
|
|
318
|
+
try {
|
|
319
|
+
JSON.parse(buffer.value);
|
|
320
|
+
return true;
|
|
321
|
+
} catch {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
})();
|
|
325
|
+
buffer.value += delta;
|
|
326
|
+
return !wasComplete;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
226
330
|
function codexOutputHasMeaningfulProgress(output: AssistantMessage): boolean {
|
|
227
331
|
return output.content.some(
|
|
228
332
|
block =>
|
|
229
|
-
(block.type === "text" && block.text.length > 0) ||
|
|
333
|
+
(block.type === "text" && block.text.length > 0) ||
|
|
334
|
+
(block.type === "thinking" && block.thinking.length > 0) ||
|
|
335
|
+
(block.type === "toolCall" &&
|
|
336
|
+
typeof block.arguments === "object" &&
|
|
337
|
+
block.arguments !== null &&
|
|
338
|
+
!Array.isArray(block.arguments) &&
|
|
339
|
+
Object.keys(block.arguments).length > 0),
|
|
230
340
|
);
|
|
231
341
|
}
|
|
232
342
|
|
|
343
|
+
function isPlainJsonObject(value: unknown): value is Record<string, unknown> {
|
|
344
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function isCompleteJsonObject(value: string): boolean {
|
|
348
|
+
try {
|
|
349
|
+
return isPlainJsonObject(JSON.parse(value));
|
|
350
|
+
} catch {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
233
355
|
type CodexTransport = "sse" | "websocket";
|
|
234
356
|
interface CodexInitialTransport {
|
|
235
357
|
eventStream: AsyncGenerator<Record<string, unknown>>;
|
|
@@ -241,7 +363,15 @@ interface CodexInitialTransport {
|
|
|
241
363
|
}
|
|
242
364
|
type CodexEventItem = ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall | ResponseCustomToolCall;
|
|
243
365
|
type CodexThinkingBlock = ThinkingContent & { summaryBuffer: string; rawBuffer: string; summaryStarted: boolean };
|
|
244
|
-
type CodexOutputBlock =
|
|
366
|
+
type CodexOutputBlock =
|
|
367
|
+
| CodexThinkingBlock
|
|
368
|
+
| TextContent
|
|
369
|
+
| (ToolCall & {
|
|
370
|
+
partialJson: string;
|
|
371
|
+
doneInput?: string;
|
|
372
|
+
argumentsComplete?: boolean;
|
|
373
|
+
argumentsAuthoritative?: boolean;
|
|
374
|
+
});
|
|
245
375
|
export interface OpenAICodexWebSocketDebugStats {
|
|
246
376
|
fullContextRequests: number;
|
|
247
377
|
deltaRequests: number;
|
|
@@ -270,6 +400,7 @@ type CodexWebSocketSessionState = {
|
|
|
270
400
|
interface CodexProviderSessionState extends ProviderSessionState {
|
|
271
401
|
webSocketSessions: Map<string, CodexWebSocketSessionState>;
|
|
272
402
|
webSocketPublicToPrivate: Map<string, string>;
|
|
403
|
+
disabledWebSocketPublicSessions: Set<string>;
|
|
273
404
|
}
|
|
274
405
|
|
|
275
406
|
interface CodexRequestContext {
|
|
@@ -336,6 +467,7 @@ interface CodexStreamRuntime {
|
|
|
336
467
|
transport: CodexTransport;
|
|
337
468
|
websocketState?: CodexWebSocketSessionState;
|
|
338
469
|
currentItem: CodexEventItem | null;
|
|
470
|
+
currentItemOutputIndex?: number;
|
|
339
471
|
currentBlock: CodexOutputBlock | null;
|
|
340
472
|
nativeOutputItems: Array<Record<string, unknown>>;
|
|
341
473
|
websocketStreamRetries: number;
|
|
@@ -362,6 +494,8 @@ interface CodexStreamRuntime {
|
|
|
362
494
|
finalizedToolCallIds: Set<string>;
|
|
363
495
|
/** Event types whose degraded non-string increment was already diagnosed. */
|
|
364
496
|
degradedIncrementDiagnostics: Set<string>;
|
|
497
|
+
/** An argument event identified a different active item; never salvage this response. */
|
|
498
|
+
toolArgumentCorrelationFailed: boolean;
|
|
365
499
|
}
|
|
366
500
|
|
|
367
501
|
interface CodexStreamProcessingContext {
|
|
@@ -429,12 +563,14 @@ function createCodexProviderSessionState(): CodexProviderSessionState {
|
|
|
429
563
|
const state: CodexProviderSessionState = {
|
|
430
564
|
webSocketSessions: new Map(),
|
|
431
565
|
webSocketPublicToPrivate: new Map(),
|
|
566
|
+
disabledWebSocketPublicSessions: new Set(),
|
|
432
567
|
close: () => {
|
|
433
568
|
for (const session of state.webSocketSessions.values()) {
|
|
434
569
|
session.connection?.close("session_disposed");
|
|
435
570
|
}
|
|
436
571
|
state.webSocketSessions.clear();
|
|
437
572
|
state.webSocketPublicToPrivate.clear();
|
|
573
|
+
state.disabledWebSocketPublicSessions.clear();
|
|
438
574
|
},
|
|
439
575
|
};
|
|
440
576
|
return state;
|
|
@@ -680,6 +816,8 @@ function removeTransientBlockIndices(output: AssistantMessage): void {
|
|
|
680
816
|
if (block.type === "toolCall") {
|
|
681
817
|
delete (block as { partialJson?: string }).partialJson;
|
|
682
818
|
delete (block as { doneInput?: string }).doneInput;
|
|
819
|
+
delete (block as { argumentsComplete?: boolean }).argumentsComplete;
|
|
820
|
+
delete (block as { argumentsAuthoritative?: boolean }).argumentsAuthoritative;
|
|
683
821
|
}
|
|
684
822
|
}
|
|
685
823
|
}
|
|
@@ -702,7 +840,7 @@ function createRequestSetup(options: OpenAICodexResponsesOptions | undefined): C
|
|
|
702
840
|
onIdle: () => requestAbortController.abort(),
|
|
703
841
|
onFirstItemTimeout: () => requestAbortController.abort(),
|
|
704
842
|
abortSignal: options?.signal,
|
|
705
|
-
isProgressItem:
|
|
843
|
+
isProgressItem: createCodexStreamProgressClassifier(),
|
|
706
844
|
});
|
|
707
845
|
return { requestAbortController, requestSignal, firstEventTimeoutMs, wrapCodexSseStream };
|
|
708
846
|
}
|
|
@@ -746,11 +884,27 @@ async function buildCodexRequestContext(
|
|
|
746
884
|
const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
|
|
747
885
|
const sessionKey = getCodexWebSocketSessionKey(promptCacheKey, model, accountId, baseUrl);
|
|
748
886
|
const publicSessionKey = getCodexPublicSessionKey(promptCacheKey, model, baseUrl);
|
|
749
|
-
|
|
750
|
-
providerSessionState
|
|
751
|
-
|
|
887
|
+
const effectiveSessionKey =
|
|
888
|
+
providerSessionState && sessionKey
|
|
889
|
+
? resolveCodexWebSocketSessionKey(sessionKey, publicSessionKey, providerSessionState)
|
|
890
|
+
: undefined;
|
|
891
|
+
logCodexDebug("build request context session keys", {
|
|
892
|
+
publicSessionKey,
|
|
893
|
+
effectiveSessionKey,
|
|
894
|
+
providerSessionStateMapIdentity: getProviderSessionStateIdentity(options?.providerSessionState),
|
|
895
|
+
});
|
|
752
896
|
const websocketState =
|
|
753
|
-
|
|
897
|
+
providerSessionState && effectiveSessionKey
|
|
898
|
+
? getCodexWebSocketSessionState(effectiveSessionKey, providerSessionState)
|
|
899
|
+
: undefined;
|
|
900
|
+
if (
|
|
901
|
+
websocketState &&
|
|
902
|
+
publicSessionKey &&
|
|
903
|
+
(disabledCodexPublicSessionKeys.has(publicSessionKey) ||
|
|
904
|
+
providerSessionState?.disabledWebSocketPublicSessions.has(publicSessionKey))
|
|
905
|
+
) {
|
|
906
|
+
websocketState.disableWebsocket = true;
|
|
907
|
+
}
|
|
754
908
|
|
|
755
909
|
return {
|
|
756
910
|
apiKey,
|
|
@@ -1063,6 +1217,7 @@ function createCodexStreamRuntime(initial: {
|
|
|
1063
1217
|
transport: initial.transport,
|
|
1064
1218
|
websocketState: initial.websocketState,
|
|
1065
1219
|
currentItem: null,
|
|
1220
|
+
currentItemOutputIndex: undefined,
|
|
1066
1221
|
currentBlock: null,
|
|
1067
1222
|
nativeOutputItems: [],
|
|
1068
1223
|
websocketStreamRetries: 0,
|
|
@@ -1077,6 +1232,7 @@ function createCodexStreamRuntime(initial: {
|
|
|
1077
1232
|
canSafelyReplayWebsocketOverSse: true,
|
|
1078
1233
|
finalizedToolCallIds: new Set<string>(),
|
|
1079
1234
|
degradedIncrementDiagnostics: new Set<string>(),
|
|
1235
|
+
toolArgumentCorrelationFailed: false,
|
|
1080
1236
|
};
|
|
1081
1237
|
}
|
|
1082
1238
|
|
|
@@ -1086,10 +1242,10 @@ async function processCodexResponseStream(
|
|
|
1086
1242
|
): Promise<CodexStreamCompletion> {
|
|
1087
1243
|
const { output, stream } = context;
|
|
1088
1244
|
stream.push({ type: "start", partial: output });
|
|
1245
|
+
let firstTokenTime = context.firstTokenTime;
|
|
1089
1246
|
|
|
1090
1247
|
while (true) {
|
|
1091
1248
|
try {
|
|
1092
|
-
let firstTokenTime = context.firstTokenTime;
|
|
1093
1249
|
for await (const rawEvent of runtime.eventStream) {
|
|
1094
1250
|
firstTokenTime = handleCodexStreamEvent({
|
|
1095
1251
|
...context,
|
|
@@ -1101,6 +1257,9 @@ async function processCodexResponseStream(
|
|
|
1101
1257
|
}
|
|
1102
1258
|
return { firstTokenTime };
|
|
1103
1259
|
} catch (error) {
|
|
1260
|
+
if (trySalvageCodexFinalizedToolCalls(context, runtime, error)) {
|
|
1261
|
+
return { firstTokenTime };
|
|
1262
|
+
}
|
|
1104
1263
|
const recovered = await recoverCodexStreamError(context, runtime, error);
|
|
1105
1264
|
if (!recovered) {
|
|
1106
1265
|
throw error;
|
|
@@ -1109,6 +1268,118 @@ async function processCodexResponseStream(
|
|
|
1109
1268
|
}
|
|
1110
1269
|
}
|
|
1111
1270
|
|
|
1271
|
+
function trySalvageCodexFinalizedToolCalls(
|
|
1272
|
+
context: CodexStreamProcessingContext,
|
|
1273
|
+
runtime: CodexStreamRuntime,
|
|
1274
|
+
error: unknown,
|
|
1275
|
+
): boolean {
|
|
1276
|
+
const toolCalls = context.output.content.filter((block): block is ToolCall => block.type === "toolCall");
|
|
1277
|
+
const activeToolCall =
|
|
1278
|
+
runtime.currentItem?.type === "function_call" && runtime.currentBlock?.type === "toolCall"
|
|
1279
|
+
? runtime.currentBlock
|
|
1280
|
+
: undefined;
|
|
1281
|
+
const completeActiveToolCall =
|
|
1282
|
+
activeToolCall?.argumentsComplete === true &&
|
|
1283
|
+
(activeToolCall.argumentsAuthoritative === true || Object.keys(activeToolCall.arguments).length > 0);
|
|
1284
|
+
const hasCompleteArguments = toolCalls.every(
|
|
1285
|
+
toolCall =>
|
|
1286
|
+
runtime.finalizedToolCallIds.has(toolCall.id) || (toolCall === activeToolCall && completeActiveToolCall),
|
|
1287
|
+
);
|
|
1288
|
+
const isIdleStall =
|
|
1289
|
+
error instanceof Error && error.message === "OpenAI Codex SSE stream stalled while waiting for the next event";
|
|
1290
|
+
const canSalvageFinalizedCall =
|
|
1291
|
+
isCodexTransientStreamClose(error) &&
|
|
1292
|
+
!runtime.toolArgumentCorrelationFailed &&
|
|
1293
|
+
toolCalls.length > 0 &&
|
|
1294
|
+
(runtime.currentBlock === null ||
|
|
1295
|
+
(runtime.currentItem?.type === "reasoning" && runtime.currentBlock?.type === "thinking")) &&
|
|
1296
|
+
context.output.content.every(block => block.type === "thinking" || block.type === "toolCall") &&
|
|
1297
|
+
toolCalls.every(toolCall => runtime.finalizedToolCallIds.has(toolCall.id));
|
|
1298
|
+
const canSalvageCompleteArguments =
|
|
1299
|
+
(isCodexTransientStreamClose(error) || isIdleStall) &&
|
|
1300
|
+
!runtime.toolArgumentCorrelationFailed &&
|
|
1301
|
+
toolCalls.length > 0 &&
|
|
1302
|
+
context.output.content.every(block => block.type === "thinking" || block.type === "toolCall") &&
|
|
1303
|
+
hasCompleteArguments;
|
|
1304
|
+
if (!canSalvageFinalizedCall && !canSalvageCompleteArguments) {
|
|
1305
|
+
return false;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
if (canSalvageFinalizedCall && runtime.currentBlock?.type === "thinking") {
|
|
1309
|
+
context.stream.push({
|
|
1310
|
+
type: "thinking_end",
|
|
1311
|
+
contentIndex: context.output.content.length - 1,
|
|
1312
|
+
content: runtime.currentBlock.thinking,
|
|
1313
|
+
partial: context.output,
|
|
1314
|
+
});
|
|
1315
|
+
runtime.currentItem = null;
|
|
1316
|
+
runtime.currentItemOutputIndex = undefined;
|
|
1317
|
+
runtime.currentBlock = null;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
if (canSalvageCompleteArguments && activeToolCall && !runtime.finalizedToolCallIds.has(activeToolCall.id)) {
|
|
1321
|
+
const item = runtime.currentItem;
|
|
1322
|
+
const toolCall: ToolCall = {
|
|
1323
|
+
type: "toolCall",
|
|
1324
|
+
id: activeToolCall.id,
|
|
1325
|
+
name: activeToolCall.name,
|
|
1326
|
+
arguments: activeToolCall.arguments,
|
|
1327
|
+
};
|
|
1328
|
+
const rawPartialJson = activeToolCall.partialJson;
|
|
1329
|
+
captureUnicodeEscapeEvidence(toolCall, rawPartialJson);
|
|
1330
|
+
Object.assign(activeToolCall, toolCall);
|
|
1331
|
+
captureUnicodeEscapeEvidence(activeToolCall, rawPartialJson);
|
|
1332
|
+
delete (activeToolCall as { partialJson?: string }).partialJson;
|
|
1333
|
+
delete (activeToolCall as { argumentsComplete?: boolean }).argumentsComplete;
|
|
1334
|
+
delete (activeToolCall as { argumentsAuthoritative?: boolean }).argumentsAuthoritative;
|
|
1335
|
+
runtime.finalizedToolCallIds.add(toolCall.id);
|
|
1336
|
+
runtime.nativeOutputItems.push({
|
|
1337
|
+
...item,
|
|
1338
|
+
arguments: JSON.stringify(toolCall.arguments),
|
|
1339
|
+
status: "completed",
|
|
1340
|
+
} as Record<string, unknown>);
|
|
1341
|
+
context.stream.push({
|
|
1342
|
+
type: "toolcall_end",
|
|
1343
|
+
contentIndex: context.output.content.length - 1,
|
|
1344
|
+
toolCall,
|
|
1345
|
+
partial: context.output,
|
|
1346
|
+
});
|
|
1347
|
+
runtime.currentItem = null;
|
|
1348
|
+
runtime.currentBlock = null;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
context.output.stopReason = "toolUse";
|
|
1352
|
+
context.output.errorCode = canSalvageFinalizedCall
|
|
1353
|
+
? CODEX_SALVAGED_STREAM_CLOSE_ERROR_CODE
|
|
1354
|
+
: CODEX_SALVAGED_COMPLETE_TOOL_ARGUMENTS_ERROR_CODE;
|
|
1355
|
+
runtime.sawTerminalEvent = true;
|
|
1356
|
+
logCodexDebug("codex stream closed after complete tool arguments; salvaging tool-use response", {
|
|
1357
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1358
|
+
toolCallCount: toolCalls.length,
|
|
1359
|
+
transport: runtime.transport,
|
|
1360
|
+
});
|
|
1361
|
+
if (context.requestContext.websocketState) {
|
|
1362
|
+
resetCodexWebSocketAppendState(context.requestContext.websocketState);
|
|
1363
|
+
}
|
|
1364
|
+
return true;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function isCodexTransientStreamClose(error: unknown): boolean {
|
|
1368
|
+
if (!(error instanceof Error)) return false;
|
|
1369
|
+
const providerMessage = (error as CodexProviderStreamError).providerMessage;
|
|
1370
|
+
const message = (providerMessage || error.message).toLowerCase();
|
|
1371
|
+
const providerCode =
|
|
1372
|
+
(error as CodexProviderStreamError & { providerCode?: string }).code?.toLowerCase() ??
|
|
1373
|
+
(error as { providerCode?: string }).providerCode?.toLowerCase();
|
|
1374
|
+
if (providerCode && CODEX_NON_RETRYABLE_EVENT_CODES.has(providerCode)) return false;
|
|
1375
|
+
const hasRequestTimeout = providerCode === "request_timeout" || message.includes("request_timeout");
|
|
1376
|
+
const hasClosedStreamMessage =
|
|
1377
|
+
message.includes("stream disconnected before completion") ||
|
|
1378
|
+
message.includes("stream closed before response.completed") ||
|
|
1379
|
+
message.includes("websocket closed before response completion");
|
|
1380
|
+
return hasRequestTimeout && hasClosedStreamMessage;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1112
1383
|
function handleCodexStreamEvent(args: {
|
|
1113
1384
|
model: Model<"openai-codex-responses">;
|
|
1114
1385
|
output: AssistantMessage;
|
|
@@ -1129,6 +1400,7 @@ function handleCodexStreamEvent(args: {
|
|
|
1129
1400
|
if (!firstTokenTime) firstTokenTime = Date.now();
|
|
1130
1401
|
const item = rawEvent.item as CodexEventItem;
|
|
1131
1402
|
runtime.currentItem = item;
|
|
1403
|
+
runtime.currentItemOutputIndex = typeof rawEvent.output_index === "number" ? rawEvent.output_index : undefined;
|
|
1132
1404
|
runtime.currentBlock = createOutputBlockForItem(item);
|
|
1133
1405
|
if (!runtime.currentBlock) return firstTokenTime;
|
|
1134
1406
|
const currentBlock = runtime.currentBlock;
|
|
@@ -1215,12 +1487,20 @@ function handleCodexStreamEvent(args: {
|
|
|
1215
1487
|
}
|
|
1216
1488
|
|
|
1217
1489
|
if (eventType === "response.function_call_arguments.delta") {
|
|
1490
|
+
if (!isActiveToolArgumentEvent(runtime, rawEvent)) {
|
|
1491
|
+
runtime.toolArgumentCorrelationFailed = true;
|
|
1492
|
+
return firstTokenTime;
|
|
1493
|
+
}
|
|
1218
1494
|
const delta = assertStringToolArgumentIncrement(rawEvent, "response.function_call_arguments.delta");
|
|
1219
1495
|
handleToolCallArgumentsDelta(runtime.currentItem, runtime.currentBlock, delta, stream, output, blockIndex);
|
|
1220
1496
|
return firstTokenTime;
|
|
1221
1497
|
}
|
|
1222
1498
|
|
|
1223
1499
|
if (eventType === "response.function_call_arguments.done") {
|
|
1500
|
+
if (!isActiveToolArgumentEvent(runtime, rawEvent)) {
|
|
1501
|
+
runtime.toolArgumentCorrelationFailed = true;
|
|
1502
|
+
return firstTokenTime;
|
|
1503
|
+
}
|
|
1224
1504
|
handleToolCallArgumentsDone(runtime.currentItem, runtime.currentBlock, rawEvent);
|
|
1225
1505
|
return firstTokenTime;
|
|
1226
1506
|
}
|
|
@@ -1265,13 +1545,31 @@ function createOutputBlockForItem(item: CodexEventItem): CodexOutputBlock | null
|
|
|
1265
1545
|
return { type: "text", text: "" };
|
|
1266
1546
|
}
|
|
1267
1547
|
if (item.type === "function_call") {
|
|
1268
|
-
|
|
1548
|
+
const initialArguments = item.arguments || "";
|
|
1549
|
+
let parsedArguments: Record<string, unknown> = parseStreamingJson(initialArguments);
|
|
1550
|
+
try {
|
|
1551
|
+
const parsed = JSON.parse(initialArguments);
|
|
1552
|
+
if (isPlainJsonObject(parsed)) parsedArguments = parsed;
|
|
1553
|
+
} catch {
|
|
1554
|
+
// Keep the streaming parser's partial object for deltas, but do not
|
|
1555
|
+
// treat an incomplete snapshot as executable arguments.
|
|
1556
|
+
}
|
|
1557
|
+
const hasValidInitialArguments = isCompleteJsonObject(initialArguments);
|
|
1558
|
+
const block: ToolCall & {
|
|
1559
|
+
partialJson: string;
|
|
1560
|
+
argumentsComplete: boolean;
|
|
1561
|
+
argumentsAuthoritative: boolean;
|
|
1562
|
+
} = {
|
|
1269
1563
|
type: "toolCall",
|
|
1270
1564
|
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
1271
1565
|
name: codexToolCanonicalName(item.name),
|
|
1272
|
-
arguments: {},
|
|
1273
|
-
partialJson:
|
|
1566
|
+
arguments: hasValidInitialArguments ? parsedArguments : {},
|
|
1567
|
+
partialJson: initialArguments,
|
|
1568
|
+
argumentsComplete: hasValidInitialArguments && Object.keys(parsedArguments).length > 0,
|
|
1569
|
+
argumentsAuthoritative: false,
|
|
1274
1570
|
};
|
|
1571
|
+
captureUnicodeEscapeEvidence(block, initialArguments);
|
|
1572
|
+
return block;
|
|
1275
1573
|
}
|
|
1276
1574
|
if (item.type === "custom_tool_call") {
|
|
1277
1575
|
const initialInput: unknown = item.input;
|
|
@@ -1440,8 +1738,14 @@ function handleToolCallArgumentsDelta(
|
|
|
1440
1738
|
blockIndex: () => number,
|
|
1441
1739
|
): void {
|
|
1442
1740
|
if (currentItem?.type !== "function_call" || currentBlock?.type !== "toolCall") return;
|
|
1741
|
+
if (currentBlock.argumentsComplete === true && delta.trim().length > 0) {
|
|
1742
|
+
currentBlock.argumentsComplete = false;
|
|
1743
|
+
}
|
|
1443
1744
|
currentBlock.partialJson += delta;
|
|
1444
1745
|
currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
|
|
1746
|
+
currentBlock.argumentsComplete = isCompleteJsonObject(currentBlock.partialJson);
|
|
1747
|
+
currentBlock.argumentsAuthoritative =
|
|
1748
|
+
currentBlock.argumentsAuthoritative || (currentBlock.argumentsComplete && delta.trim().length > 0);
|
|
1445
1749
|
stream.push({ type: "toolcall_delta", contentIndex: blockIndex(), delta, partial: output });
|
|
1446
1750
|
}
|
|
1447
1751
|
|
|
@@ -1455,10 +1759,25 @@ function handleToolCallArgumentsDone(
|
|
|
1455
1759
|
if (typeof args === "string") {
|
|
1456
1760
|
currentBlock.partialJson = args;
|
|
1457
1761
|
currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
|
|
1762
|
+
currentBlock.argumentsComplete = isCompleteJsonObject(currentBlock.partialJson);
|
|
1763
|
+
currentBlock.argumentsAuthoritative = currentBlock.argumentsComplete;
|
|
1458
1764
|
captureUnicodeEscapeEvidence(currentBlock, args);
|
|
1459
1765
|
}
|
|
1460
1766
|
}
|
|
1461
1767
|
|
|
1768
|
+
function isActiveToolArgumentEvent(runtime: CodexStreamRuntime, rawEvent: Record<string, unknown>): boolean {
|
|
1769
|
+
if (runtime.currentItem?.type !== "function_call" || runtime.currentBlock?.type !== "toolCall") return false;
|
|
1770
|
+
const itemId = rawEvent.item_id;
|
|
1771
|
+
if (typeof itemId === "string" && itemId !== runtime.currentItem.id) return false;
|
|
1772
|
+
const callId = rawEvent.call_id;
|
|
1773
|
+
if (typeof callId === "string" && callId !== runtime.currentItem.call_id) return false;
|
|
1774
|
+
const outputIndex = rawEvent.output_index;
|
|
1775
|
+
if (typeof outputIndex === "number" && outputIndex !== runtime.currentItemOutputIndex) {
|
|
1776
|
+
return false;
|
|
1777
|
+
}
|
|
1778
|
+
return true;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1462
1781
|
function handleCustomToolCallInputDelta(
|
|
1463
1782
|
currentItem: CodexEventItem | null,
|
|
1464
1783
|
currentBlock: CodexOutputBlock | null,
|
|
@@ -1598,6 +1917,8 @@ function handleOutputItemDone(
|
|
|
1598
1917
|
Object.assign(runtime.currentBlock, toolCall);
|
|
1599
1918
|
captureUnicodeEscapeEvidence(runtime.currentBlock, item.arguments);
|
|
1600
1919
|
delete (runtime.currentBlock as { partialJson?: string }).partialJson;
|
|
1920
|
+
delete (runtime.currentBlock as { argumentsComplete?: boolean }).argumentsComplete;
|
|
1921
|
+
delete (runtime.currentBlock as { argumentsAuthoritative?: boolean }).argumentsAuthoritative;
|
|
1601
1922
|
delete (runtime.currentBlock as { doneInput?: string }).doneInput;
|
|
1602
1923
|
runtime.canSafelyReplayWebsocketOverSse = false;
|
|
1603
1924
|
stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
|
|
@@ -1738,6 +2059,7 @@ async function recoverCodexStreamError(
|
|
|
1738
2059
|
if (isCodexFirstEventTimeout(error)) return false;
|
|
1739
2060
|
// A post-progress idle timeout has already consumed the stream's silence budget.
|
|
1740
2061
|
// Replaying it over another transport can leave the turn silent for another full window.
|
|
2062
|
+
// A tool-call start is progress even without reasoning, usage, or output_item.done.
|
|
1741
2063
|
if (
|
|
1742
2064
|
(codexOutputHasMeaningfulProgress(context.output) ||
|
|
1743
2065
|
(context.output.content.some(block => block.type === "thinking") &&
|
|
@@ -1805,6 +2127,7 @@ async function tryRetryWithoutForcedToolChoice(
|
|
|
1805
2127
|
runtime.sawTerminalEvent = false;
|
|
1806
2128
|
runtime.nativeOutputItems.length = 0;
|
|
1807
2129
|
runtime.finalizedToolCallIds.clear();
|
|
2130
|
+
runtime.toolArgumentCorrelationFailed = false;
|
|
1808
2131
|
resetOutputState(context.output);
|
|
1809
2132
|
context.firstTokenTime = undefined;
|
|
1810
2133
|
|
|
@@ -1903,6 +2226,7 @@ async function tryReconnectCodexWebSocketOnConnectionLimit(
|
|
|
1903
2226
|
runtime.currentBlock = null;
|
|
1904
2227
|
runtime.nativeOutputItems.length = 0;
|
|
1905
2228
|
runtime.finalizedToolCallIds.clear();
|
|
2229
|
+
runtime.toolArgumentCorrelationFailed = false;
|
|
1906
2230
|
resetOutputState(context.output);
|
|
1907
2231
|
context.firstTokenTime = undefined;
|
|
1908
2232
|
recordCodexWebSocketFailure(websocketState, true);
|
|
@@ -1940,14 +2264,14 @@ async function tryRecoverCodexPreviousResponseNotFound(
|
|
|
1940
2264
|
// validation fault: clearing session metadata and resending the identical
|
|
1941
2265
|
// body cannot fix it.
|
|
1942
2266
|
!runtime.sentPreviousResponseId ||
|
|
2267
|
+
// Managed fallback forces streamMaxRetries to 0; this replay is exempt
|
|
2268
|
+
// from that budget and bounded by previousResponseRecoveryAttempted.
|
|
1943
2269
|
runtime.previousResponseRecoveryAttempted ||
|
|
1944
2270
|
!websocketState ||
|
|
1945
|
-
context.options?.fallbackManaged ||
|
|
1946
2271
|
context.options?.disableProviderRetries ||
|
|
1947
2272
|
runtime.transport !== "websocket" ||
|
|
1948
2273
|
context.output.content.length > 0 ||
|
|
1949
|
-
context.options?.signal?.aborted
|
|
1950
|
-
runtime.providerRetryAttempt >= resolveRetryBudget(context.options?.streamMaxRetries, CODEX_MAX_RETRIES)
|
|
2274
|
+
context.options?.signal?.aborted
|
|
1951
2275
|
) {
|
|
1952
2276
|
return false;
|
|
1953
2277
|
}
|
|
@@ -1961,6 +2285,7 @@ async function tryRecoverCodexPreviousResponseNotFound(
|
|
|
1961
2285
|
runtime.sawTerminalEvent = false;
|
|
1962
2286
|
runtime.nativeOutputItems.length = 0;
|
|
1963
2287
|
runtime.finalizedToolCallIds.clear();
|
|
2288
|
+
runtime.toolArgumentCorrelationFailed = false;
|
|
1964
2289
|
resetOutputState(context.output);
|
|
1965
2290
|
context.firstTokenTime = undefined;
|
|
1966
2291
|
|
|
@@ -2021,6 +2346,7 @@ async function tryReplayWebsocketFailureOverSse(
|
|
|
2021
2346
|
runtime.currentBlock = null;
|
|
2022
2347
|
runtime.nativeOutputItems.length = 0;
|
|
2023
2348
|
runtime.finalizedToolCallIds.clear();
|
|
2349
|
+
runtime.toolArgumentCorrelationFailed = false;
|
|
2024
2350
|
resetOutputState(context.output);
|
|
2025
2351
|
context.firstTokenTime = undefined;
|
|
2026
2352
|
}
|
|
@@ -2064,6 +2390,7 @@ async function tryRetryCodexProviderError(
|
|
|
2064
2390
|
runtime.sawTerminalEvent = false;
|
|
2065
2391
|
runtime.nativeOutputItems.length = 0;
|
|
2066
2392
|
runtime.finalizedToolCallIds.clear();
|
|
2393
|
+
runtime.toolArgumentCorrelationFailed = false;
|
|
2067
2394
|
resetOutputState(context.output);
|
|
2068
2395
|
context.firstTokenTime = undefined;
|
|
2069
2396
|
await scheduler.wait(CODEX_RETRY_DELAY_MS * runtime.providerRetryAttempt, {
|
|
@@ -2127,7 +2454,18 @@ async function handleCodexStreamFailure(
|
|
|
2127
2454
|
}
|
|
2128
2455
|
output.stopReason = context.options?.signal?.aborted ? "aborted" : "error";
|
|
2129
2456
|
output.errorStatus = extractHttpStatusFromError(error);
|
|
2130
|
-
|
|
2457
|
+
const transportFailure = transportFailureFacts(error);
|
|
2458
|
+
const typedProviderCode =
|
|
2459
|
+
error instanceof CodexProviderStreamError && CODEX_TYPED_TRANSPORT_PROVIDER_CODES.has(error.code ?? "")
|
|
2460
|
+
? error.code
|
|
2461
|
+
: undefined;
|
|
2462
|
+
output.transportFailure = typedProviderCode
|
|
2463
|
+
? {
|
|
2464
|
+
...(transportFailure ?? { kind: "transport" as const }),
|
|
2465
|
+
providerCode: typedProviderCode,
|
|
2466
|
+
...(error instanceof CodexProviderStreamError && error.deterministicVeto ? { retryMaxAttempts: 1 } : {}),
|
|
2467
|
+
}
|
|
2468
|
+
: transportFailure;
|
|
2131
2469
|
output.errorMessage = await finalizeErrorMessage(error, context.requestContext.rawRequestDump);
|
|
2132
2470
|
output.duration = Date.now() - context.startTime;
|
|
2133
2471
|
if (context.firstTokenTime) {
|
|
@@ -2245,11 +2583,14 @@ export async function prewarmOpenAICodexResponses(
|
|
|
2245
2583
|
const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
|
|
2246
2584
|
const sessionKey = getCodexWebSocketSessionKey(promptCacheKey, model, accountId, baseUrl);
|
|
2247
2585
|
const publicSessionKey = getCodexPublicSessionKey(promptCacheKey, model, baseUrl);
|
|
2248
|
-
if (publicSessionKey && sessionKey) {
|
|
2249
|
-
providerSessionState?.webSocketPublicToPrivate.set(publicSessionKey, sessionKey);
|
|
2250
|
-
}
|
|
2251
2586
|
if (!sessionKey || !providerSessionState) return;
|
|
2252
|
-
const
|
|
2587
|
+
const effectiveSessionKey = resolveCodexWebSocketSessionKey(sessionKey, publicSessionKey, providerSessionState);
|
|
2588
|
+
logCodexDebug("prewarm session keys", {
|
|
2589
|
+
publicSessionKey,
|
|
2590
|
+
effectiveSessionKey,
|
|
2591
|
+
providerSessionStateMapIdentity: getProviderSessionStateIdentity(options?.providerSessionState),
|
|
2592
|
+
});
|
|
2593
|
+
const state = getCodexWebSocketSessionState(effectiveSessionKey, providerSessionState);
|
|
2253
2594
|
if (!shouldUseCodexWebSocket(model, state, options?.preferWebsockets)) return;
|
|
2254
2595
|
const headers = logger.time(
|
|
2255
2596
|
"prewarmCodex:createHeaders",
|
|
@@ -2261,7 +2602,7 @@ export async function prewarmOpenAICodexResponses(
|
|
|
2261
2602
|
"websocket",
|
|
2262
2603
|
state,
|
|
2263
2604
|
);
|
|
2264
|
-
|
|
2605
|
+
const connectionPromise = logger.time(
|
|
2265
2606
|
"prewarmCodex:establishWs",
|
|
2266
2607
|
getOrCreateCodexWebSocketConnection,
|
|
2267
2608
|
state,
|
|
@@ -2269,7 +2610,35 @@ export async function prewarmOpenAICodexResponses(
|
|
|
2269
2610
|
headers,
|
|
2270
2611
|
options?.signal,
|
|
2271
2612
|
);
|
|
2272
|
-
|
|
2613
|
+
const attemptedConnection = state.connection;
|
|
2614
|
+
try {
|
|
2615
|
+
await connectionPromise;
|
|
2616
|
+
} catch (error) {
|
|
2617
|
+
const websocketError = error instanceof Error ? error : new Error(String(error));
|
|
2618
|
+
const superseded =
|
|
2619
|
+
attemptedConnection?.wasClosedLocally() === true ||
|
|
2620
|
+
(attemptedConnection !== undefined && state.connection !== attemptedConnection);
|
|
2621
|
+
const fatalPrewarmFailure =
|
|
2622
|
+
isCodexWebSocketFatalError(websocketError) &&
|
|
2623
|
+
!websocketError.message.toLowerCase().includes("connection timeout") &&
|
|
2624
|
+
!/^.*websocket closed before open \((?:1000|1001|1005)\)$/i.test(websocketError.message) &&
|
|
2625
|
+
!superseded &&
|
|
2626
|
+
state.connection === attemptedConnection;
|
|
2627
|
+
if (fatalPrewarmFailure) {
|
|
2628
|
+
recordCodexWebSocketFailure(state, true);
|
|
2629
|
+
if (publicSessionKey) providerSessionState.disabledWebSocketPublicSessions.add(publicSessionKey);
|
|
2630
|
+
if (publicSessionKey) disabledCodexPublicSessionKeys.add(publicSessionKey);
|
|
2631
|
+
} else if (superseded) {
|
|
2632
|
+
logCodexDebug("ignoring superseded Codex websocket prewarm failure", {
|
|
2633
|
+
error: websocketError.message,
|
|
2634
|
+
publicSessionKey,
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
throw error;
|
|
2638
|
+
}
|
|
2639
|
+
if (state.connection === attemptedConnection) {
|
|
2640
|
+
state.prewarmed = true;
|
|
2641
|
+
}
|
|
2273
2642
|
}
|
|
2274
2643
|
|
|
2275
2644
|
function getCodexWebSocketSessionKey(
|
|
@@ -2283,6 +2652,17 @@ function getCodexWebSocketSessionKey(
|
|
|
2283
2652
|
return `${accountId ?? "opaque"}:${baseUrl}:${model.id}:${promptCacheKey}`;
|
|
2284
2653
|
}
|
|
2285
2654
|
|
|
2655
|
+
function resolveCodexWebSocketSessionKey(
|
|
2656
|
+
sessionKey: string,
|
|
2657
|
+
publicSessionKey: string | undefined,
|
|
2658
|
+
providerSessionState: CodexProviderSessionState,
|
|
2659
|
+
): string {
|
|
2660
|
+
if (publicSessionKey) {
|
|
2661
|
+
providerSessionState.webSocketPublicToPrivate.set(publicSessionKey, sessionKey);
|
|
2662
|
+
}
|
|
2663
|
+
return sessionKey;
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2286
2666
|
function getCodexPublicSessionKey(
|
|
2287
2667
|
sessionId: string | undefined,
|
|
2288
2668
|
model: Model<"openai-codex-responses">,
|
|
@@ -2548,6 +2928,7 @@ class CodexWebSocketConnection {
|
|
|
2548
2928
|
#waiters: Array<() => void> = [];
|
|
2549
2929
|
#connectPromise?: Promise<void>;
|
|
2550
2930
|
#activeRequest = false;
|
|
2931
|
+
#localCloseReason?: string;
|
|
2551
2932
|
|
|
2552
2933
|
constructor(url: string, headers: Record<string, string>, options: CodexWebSocketConnectionOptions) {
|
|
2553
2934
|
this.#url = url;
|
|
@@ -2560,11 +2941,16 @@ class CodexWebSocketConnection {
|
|
|
2560
2941
|
return this.#socket?.readyState === WebSocket.OPEN;
|
|
2561
2942
|
}
|
|
2562
2943
|
|
|
2944
|
+
wasClosedLocally(): boolean {
|
|
2945
|
+
return this.#localCloseReason !== undefined;
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2563
2948
|
matchesAuth(headers: Record<string, string>): boolean {
|
|
2564
2949
|
return this.#headers.authorization === headers.authorization;
|
|
2565
2950
|
}
|
|
2566
2951
|
|
|
2567
2952
|
close(reason = "done"): void {
|
|
2953
|
+
this.#localCloseReason = reason;
|
|
2568
2954
|
if (
|
|
2569
2955
|
this.#socket &&
|
|
2570
2956
|
(this.#socket.readyState === WebSocket.OPEN || this.#socket.readyState === WebSocket.CONNECTING)
|
|
@@ -2790,14 +3176,17 @@ class CodexWebSocketConnection {
|
|
|
2790
3176
|
let timedOut = false;
|
|
2791
3177
|
let timeout: NodeJS.Timeout | undefined;
|
|
2792
3178
|
if (timeoutMs !== undefined && timeoutMs > 0) {
|
|
2793
|
-
timeout = setTimeout(
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
3179
|
+
timeout = setTimeout(
|
|
3180
|
+
() => {
|
|
3181
|
+
timedOut = true;
|
|
3182
|
+
const waiterIndex = this.#waiters.indexOf(resolve);
|
|
3183
|
+
if (waiterIndex >= 0) {
|
|
3184
|
+
this.#waiters.splice(waiterIndex, 1);
|
|
3185
|
+
}
|
|
3186
|
+
resolve();
|
|
3187
|
+
},
|
|
3188
|
+
Math.min(timeoutMs, CODEX_MAX_TIMER_DELAY_MS),
|
|
3189
|
+
);
|
|
2801
3190
|
}
|
|
2802
3191
|
await promise;
|
|
2803
3192
|
if (timeout) clearTimeout(timeout);
|
|
@@ -3236,6 +3625,7 @@ function getCodexEventErrorMessage(rawEvent: Record<string, unknown>): string {
|
|
|
3236
3625
|
|
|
3237
3626
|
class CodexProviderStreamError extends Error {
|
|
3238
3627
|
readonly retryable: boolean;
|
|
3628
|
+
readonly deterministicVeto: boolean;
|
|
3239
3629
|
readonly code?: string;
|
|
3240
3630
|
readonly credentialModelUnavailable: boolean;
|
|
3241
3631
|
/**
|
|
@@ -3249,6 +3639,7 @@ class CodexProviderStreamError extends Error {
|
|
|
3249
3639
|
constructor(
|
|
3250
3640
|
message: string,
|
|
3251
3641
|
retryable: boolean,
|
|
3642
|
+
deterministicVeto: boolean,
|
|
3252
3643
|
code: string | undefined,
|
|
3253
3644
|
providerMessage: string,
|
|
3254
3645
|
credentialModelUnavailable: boolean,
|
|
@@ -3256,6 +3647,7 @@ class CodexProviderStreamError extends Error {
|
|
|
3256
3647
|
super(message);
|
|
3257
3648
|
this.name = "CodexProviderStreamError";
|
|
3258
3649
|
this.retryable = retryable;
|
|
3650
|
+
this.deterministicVeto = deterministicVeto;
|
|
3259
3651
|
this.code = code;
|
|
3260
3652
|
this.providerMessage = providerMessage;
|
|
3261
3653
|
this.credentialModelUnavailable = credentialModelUnavailable;
|
|
@@ -3265,10 +3657,7 @@ class CodexProviderStreamError extends Error {
|
|
|
3265
3657
|
function isRetryableCodexFailureEvent(rawEvent: Record<string, unknown>): boolean {
|
|
3266
3658
|
const code = getCodexEventErrorCode(rawEvent).toLowerCase();
|
|
3267
3659
|
const message = getCodexEventErrorMessage(rawEvent);
|
|
3268
|
-
if (
|
|
3269
|
-
(code && CODEX_NON_RETRYABLE_EVENT_CODES.has(code)) ||
|
|
3270
|
-
(!!message && CODEX_NON_RETRYABLE_EVENT_MESSAGE.test(message))
|
|
3271
|
-
) {
|
|
3660
|
+
if (isCodexDeterministicVeto(code, message)) {
|
|
3272
3661
|
return false;
|
|
3273
3662
|
}
|
|
3274
3663
|
if (code && CODEX_RETRYABLE_EVENT_CODES.has(code)) {
|
|
@@ -3277,9 +3666,17 @@ function isRetryableCodexFailureEvent(rawEvent: Record<string, unknown>): boolea
|
|
|
3277
3666
|
return !!message && CODEX_RETRYABLE_EVENT_MESSAGE.test(message);
|
|
3278
3667
|
}
|
|
3279
3668
|
|
|
3669
|
+
function isCodexDeterministicVeto(code: string, message: string): boolean {
|
|
3670
|
+
return (
|
|
3671
|
+
(code && CODEX_NON_RETRYABLE_EVENT_CODES.has(code)) ||
|
|
3672
|
+
(!!message && CODEX_NON_RETRYABLE_EVENT_MESSAGE.test(message))
|
|
3673
|
+
);
|
|
3674
|
+
}
|
|
3675
|
+
|
|
3280
3676
|
function createCodexProviderStreamError(rawEvent: Record<string, unknown>): CodexProviderStreamError {
|
|
3281
3677
|
const code = getCodexEventErrorCode(rawEvent);
|
|
3282
3678
|
const message = getCodexEventErrorMessage(rawEvent);
|
|
3679
|
+
const deterministicVeto = isCodexDeterministicVeto(code.toLowerCase(), message);
|
|
3283
3680
|
const formattedMessage =
|
|
3284
3681
|
typeof rawEvent.type === "string" && rawEvent.type === "error"
|
|
3285
3682
|
? formatCodexErrorEvent(rawEvent, code, message)
|
|
@@ -3287,6 +3684,7 @@ function createCodexProviderStreamError(rawEvent: Record<string, unknown>): Code
|
|
|
3287
3684
|
return new CodexProviderStreamError(
|
|
3288
3685
|
formattedMessage,
|
|
3289
3686
|
isRetryableCodexFailureEvent(rawEvent),
|
|
3687
|
+
deterministicVeto,
|
|
3290
3688
|
code || undefined,
|
|
3291
3689
|
message,
|
|
3292
3690
|
isCodexAccountModelUnavailable(message, code),
|