@gajae-code/ai 0.18.5 → 0.18.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/types/auth-storage.d.ts +8 -1
  3. package/dist/types/providers/cursor.d.ts +6 -3
  4. package/dist/types/providers/kiro-api-key.d.ts +1 -0
  5. package/dist/types/providers/openai-codex-responses.d.ts +3 -0
  6. package/dist/types/types.d.ts +16 -1
  7. package/dist/types/utils/fallback-transport.d.ts +6 -0
  8. package/dist/types/utils/idle-iterator.d.ts +2 -0
  9. package/dist/types/utils/oauth/glm-zcode.d.ts +5 -0
  10. package/dist/types/utils/oauth/index.d.ts +4 -0
  11. package/package.json +3 -3
  12. package/src/auth-storage.ts +115 -39
  13. package/src/model-thinking.ts +8 -0
  14. package/src/providers/aws-eventstream.ts +2 -1
  15. package/src/providers/cursor.d.ts +6 -3
  16. package/src/providers/cursor.ts +73 -64
  17. package/src/providers/google-gemini-cli.ts +49 -3
  18. package/src/providers/kiro-api-key.d.ts +1 -0
  19. package/src/providers/kiro-api-key.ts +1 -1
  20. package/src/providers/kiro-codewhisperer.ts +164 -68
  21. package/src/providers/openai-codex-responses.d.ts +3 -0
  22. package/src/providers/openai-codex-responses.ts +589 -41
  23. package/src/providers/openai-opencodex-responses.ts +6 -1
  24. package/src/providers/openai-responses.ts +1 -1
  25. package/src/providers/register-builtins.ts +53 -5
  26. package/src/stream.ts +20 -0
  27. package/src/types.d.ts +16 -1
  28. package/src/types.ts +19 -0
  29. package/src/utils/fallback-transport.d.ts +6 -0
  30. package/src/utils/fallback-transport.ts +6 -0
  31. package/src/utils/idle-iterator.d.ts +2 -0
  32. package/src/utils/idle-iterator.ts +13 -3
  33. package/src/utils/oauth/glm-zcode.d.ts +5 -0
  34. package/src/utils/oauth/glm-zcode.ts +8 -1
  35. package/src/utils/oauth/index.ts +11 -1
  36. package/src/utils/validation.ts +5 -0
@@ -52,7 +52,11 @@ import {
52
52
  sanitizeOpenAIResponsesHistoryItemsForReplay,
53
53
  } from "../utils";
54
54
  import { AssistantMessageEventStream } from "../utils/event-stream";
55
- import { STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE, transportFailureFacts } from "../utils/fallback-transport";
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(["model_error", "server_error", "internal_error"]);
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",
@@ -172,15 +190,41 @@ const CODEX_NON_RETRYABLE_EVENT_CODES = new Set([
172
190
  ]);
173
191
  const CODEX_NON_RETRYABLE_EVENT_MESSAGE =
174
192
  /invalid[_ -]function[_ -]parameters|invalid schema for function|invalid[_ -]tool[_ -]schema|schema must have type ["']?object["']?|request blocked[^\n]*invalid[_ -]prompt|code=invalid[_ -]prompt/i;
193
+ const CODEX_EXPLICIT_TERMINAL_VETO_CODES = new Set([
194
+ "invalid_function_parameters",
195
+ "invalid_schema",
196
+ "invalid_tool_schema",
197
+ "invalid_prompt",
198
+ ]);
175
199
  const CODEX_RETRYABLE_EVENT_MESSAGE =
176
200
  /processing your request|retry your request|temporar(?:y|ily)|overloaded|service.?unavailable|internal error|server error/i;
177
201
  const CODEX_ACCOUNT_MODEL_UNAVAILABLE_MESSAGE = /\bnot supported when using codex with a chatgpt account\b/i;
178
202
  const CODEX_PROVIDER_SESSION_STATE_KEY = "openai-codex-responses";
203
+ const providerSessionStateIdentities = new WeakMap<Map<string, ProviderSessionState>, number>();
204
+ const disabledCodexPublicSessionKeys = new Set<string>();
205
+ let nextProviderSessionStateIdentity = 1;
206
+
207
+ function getProviderSessionStateIdentity(
208
+ providerSessionState: Map<string, ProviderSessionState> | undefined,
209
+ ): number | undefined {
210
+ if (!providerSessionState) return undefined;
211
+ let identity = providerSessionStateIdentities.get(providerSessionState);
212
+ if (identity === undefined) {
213
+ identity = nextProviderSessionStateIdentity++;
214
+ providerSessionStateIdentities.set(providerSessionState, identity);
215
+ }
216
+ return identity;
217
+ }
179
218
  const X_CODEX_TURN_STATE_HEADER = "x-codex-turn-state";
180
219
  const X_MODELS_ETAG_HEADER = "x-models-etag";
181
220
  const X_REASONING_INCLUDED_HEADER = "x-reasoning-included";
182
221
  /** Connection-level websocket failures that should immediately fall back to SSE without retrying. */
183
- const CODEX_WEBSOCKET_FATAL_PATTERNS = ["websocket error:", "websocket closed before open", "connection timeout"];
222
+ const CODEX_WEBSOCKET_FATAL_PATTERNS = [
223
+ "websocket error:",
224
+ "websocket closed before open",
225
+ "websocket is closed before the connection is established",
226
+ "connection timeout",
227
+ ];
184
228
  /** Max total time to spend retrying 429s with server-provided delays (5 minutes). */
185
229
  const CODEX_RATE_LIMIT_BUDGET_MS = 5 * 60 * 1000;
186
230
 
@@ -207,10 +251,7 @@ const CODEX_PROGRESS_EVENT_TYPES = new Set([
207
251
  ]);
208
252
 
209
253
  /**
210
- * A progress event must carry real semantic payload, matching the Anthropic
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.
254
+ * A progress event must carry a non-empty delta for delta events.
214
255
  * Non-delta envelope types (lifecycle, item boundaries, terminal events) count
215
256
  * as progress by type alone, as before.
216
257
  */
@@ -223,13 +264,100 @@ function isCodexStreamProgressEvent(event: unknown): boolean {
223
264
  return typeof delta === "string" && delta.length > 0;
224
265
  }
225
266
 
267
+ type CodexArgumentBuffer = { value: string };
268
+
269
+ /** @internal Exported for tests. */
270
+ export function createCodexStreamProgressClassifier(): (event: unknown) => boolean {
271
+ const argumentBuffers = new Map<string, CodexArgumentBuffer>();
272
+
273
+ const getArgumentKey = (event: Record<string, unknown>): string | undefined => {
274
+ if (typeof event.item_id === "string") return `item:${event.item_id}`;
275
+ if (typeof event.output_index === "number") return `output:${event.output_index}`;
276
+ return undefined;
277
+ };
278
+
279
+ const rememberFunctionCallItem = (event: Record<string, unknown>): void => {
280
+ if (event.type !== "response.output_item.added" || !event.item || typeof event.item !== "object") return;
281
+ const item = event.item as Record<string, unknown>;
282
+ if (item.type !== "function_call" || typeof item.arguments !== "string") return;
283
+ const buffer = { value: item.arguments };
284
+ if (typeof item.id === "string") argumentBuffers.set(`item:${item.id}`, buffer);
285
+ if (typeof event.output_index === "number") argumentBuffers.set(`output:${event.output_index}`, buffer);
286
+ };
287
+
288
+ // Reads `type` and `delta` exactly once per event: the stream handler reads the
289
+ // same parsed event afterwards, and a second read here could observe a
290
+ // different value than the one that is classified and assembled.
291
+ return event => {
292
+ if (!event || typeof event !== "object") return false;
293
+ const record = event as Record<string, unknown>;
294
+ const type = record.type;
295
+ if (typeof type !== "string" || !CODEX_PROGRESS_EVENT_TYPES.has(type)) return false;
296
+ if (type.endsWith(".delta")) {
297
+ const delta = record.delta;
298
+ if (typeof delta !== "string" || delta.length === 0) return false;
299
+ if (type !== "response.function_call_arguments.delta") return true;
300
+ return classifyArgumentDelta(record, delta);
301
+ }
302
+ rememberFunctionCallItem(record);
303
+ if (type === "response.function_call_arguments.done") {
304
+ const key = getArgumentKey(record);
305
+ const buffer = key ? argumentBuffers.get(key) : undefined;
306
+ const argumentsValue = record.arguments;
307
+ if (buffer && typeof argumentsValue === "string") buffer.value = argumentsValue;
308
+ }
309
+ return true;
310
+ };
311
+
312
+ function classifyArgumentDelta(record: Record<string, unknown>, delta: string): boolean {
313
+ const key = getArgumentKey(record);
314
+ if (!key) return true;
315
+ let buffer = argumentBuffers.get(key);
316
+ if (!buffer) {
317
+ buffer = { value: "" };
318
+ argumentBuffers.set(key, buffer);
319
+ }
320
+ const isWhitespaceOnly = delta.trim().length === 0;
321
+ const wasComplete =
322
+ isWhitespaceOnly &&
323
+ (() => {
324
+ try {
325
+ JSON.parse(buffer.value);
326
+ return true;
327
+ } catch {
328
+ return false;
329
+ }
330
+ })();
331
+ buffer.value += delta;
332
+ return !wasComplete;
333
+ }
334
+ }
335
+
226
336
  function codexOutputHasMeaningfulProgress(output: AssistantMessage): boolean {
227
337
  return output.content.some(
228
338
  block =>
229
- (block.type === "text" && block.text.length > 0) || (block.type === "thinking" && block.thinking.length > 0),
339
+ (block.type === "text" && block.text.length > 0) ||
340
+ (block.type === "thinking" && block.thinking.length > 0) ||
341
+ (block.type === "toolCall" &&
342
+ typeof block.arguments === "object" &&
343
+ block.arguments !== null &&
344
+ !Array.isArray(block.arguments) &&
345
+ Object.keys(block.arguments).length > 0),
230
346
  );
231
347
  }
232
348
 
349
+ function isPlainJsonObject(value: unknown): value is Record<string, unknown> {
350
+ return typeof value === "object" && value !== null && !Array.isArray(value);
351
+ }
352
+
353
+ function isCompleteJsonObject(value: string): boolean {
354
+ try {
355
+ return isPlainJsonObject(JSON.parse(value));
356
+ } catch {
357
+ return false;
358
+ }
359
+ }
360
+
233
361
  type CodexTransport = "sse" | "websocket";
234
362
  interface CodexInitialTransport {
235
363
  eventStream: AsyncGenerator<Record<string, unknown>>;
@@ -241,7 +369,19 @@ interface CodexInitialTransport {
241
369
  }
242
370
  type CodexEventItem = ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall | ResponseCustomToolCall;
243
371
  type CodexThinkingBlock = ThinkingContent & { summaryBuffer: string; rawBuffer: string; summaryStarted: boolean };
244
- type CodexOutputBlock = CodexThinkingBlock | TextContent | (ToolCall & { partialJson: string; doneInput?: string });
372
+ type CodexOutputBlock =
373
+ | CodexThinkingBlock
374
+ | TextContent
375
+ | (ToolCall & {
376
+ partialJson: string;
377
+ doneInput?: string;
378
+ argumentsComplete?: boolean;
379
+ argumentsAuthoritative?: boolean;
380
+ sourceItemId?: string;
381
+ sourceCallId?: string;
382
+ sourceName?: string;
383
+ sourceOutputIndex?: number;
384
+ });
245
385
  export interface OpenAICodexWebSocketDebugStats {
246
386
  fullContextRequests: number;
247
387
  deltaRequests: number;
@@ -270,6 +410,7 @@ type CodexWebSocketSessionState = {
270
410
  interface CodexProviderSessionState extends ProviderSessionState {
271
411
  webSocketSessions: Map<string, CodexWebSocketSessionState>;
272
412
  webSocketPublicToPrivate: Map<string, string>;
413
+ disabledWebSocketPublicSessions: Set<string>;
273
414
  }
274
415
 
275
416
  interface CodexRequestContext {
@@ -336,8 +477,10 @@ interface CodexStreamRuntime {
336
477
  transport: CodexTransport;
337
478
  websocketState?: CodexWebSocketSessionState;
338
479
  currentItem: CodexEventItem | null;
480
+ currentItemOutputIndex?: number;
339
481
  currentBlock: CodexOutputBlock | null;
340
482
  nativeOutputItems: Array<Record<string, unknown>>;
483
+ nativeOutputItemOutputIndexes: Array<number | undefined>;
341
484
  websocketStreamRetries: number;
342
485
  providerRetryAttempt: number;
343
486
  toolChoiceFallbackAttempted: boolean;
@@ -362,6 +505,8 @@ interface CodexStreamRuntime {
362
505
  finalizedToolCallIds: Set<string>;
363
506
  /** Event types whose degraded non-string increment was already diagnosed. */
364
507
  degradedIncrementDiagnostics: Set<string>;
508
+ /** An argument event identified a different active item; never salvage this response. */
509
+ toolArgumentCorrelationFailed: boolean;
365
510
  }
366
511
 
367
512
  interface CodexStreamProcessingContext {
@@ -429,12 +574,14 @@ function createCodexProviderSessionState(): CodexProviderSessionState {
429
574
  const state: CodexProviderSessionState = {
430
575
  webSocketSessions: new Map(),
431
576
  webSocketPublicToPrivate: new Map(),
577
+ disabledWebSocketPublicSessions: new Set(),
432
578
  close: () => {
433
579
  for (const session of state.webSocketSessions.values()) {
434
580
  session.connection?.close("session_disposed");
435
581
  }
436
582
  state.webSocketSessions.clear();
437
583
  state.webSocketPublicToPrivate.clear();
584
+ state.disabledWebSocketPublicSessions.clear();
438
585
  },
439
586
  };
440
587
  return state;
@@ -680,6 +827,12 @@ function removeTransientBlockIndices(output: AssistantMessage): void {
680
827
  if (block.type === "toolCall") {
681
828
  delete (block as { partialJson?: string }).partialJson;
682
829
  delete (block as { doneInput?: string }).doneInput;
830
+ delete (block as { argumentsComplete?: boolean }).argumentsComplete;
831
+ delete (block as { argumentsAuthoritative?: boolean }).argumentsAuthoritative;
832
+ delete (block as { sourceItemId?: string }).sourceItemId;
833
+ delete (block as { sourceCallId?: string }).sourceCallId;
834
+ delete (block as { sourceName?: string }).sourceName;
835
+ delete (block as { sourceOutputIndex?: number }).sourceOutputIndex;
683
836
  }
684
837
  }
685
838
  }
@@ -702,7 +855,7 @@ function createRequestSetup(options: OpenAICodexResponsesOptions | undefined): C
702
855
  onIdle: () => requestAbortController.abort(),
703
856
  onFirstItemTimeout: () => requestAbortController.abort(),
704
857
  abortSignal: options?.signal,
705
- isProgressItem: isCodexStreamProgressEvent,
858
+ isProgressItem: createCodexStreamProgressClassifier(),
706
859
  });
707
860
  return { requestAbortController, requestSignal, firstEventTimeoutMs, wrapCodexSseStream };
708
861
  }
@@ -746,11 +899,27 @@ async function buildCodexRequestContext(
746
899
  const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
747
900
  const sessionKey = getCodexWebSocketSessionKey(promptCacheKey, model, accountId, baseUrl);
748
901
  const publicSessionKey = getCodexPublicSessionKey(promptCacheKey, model, baseUrl);
749
- if (sessionKey && publicSessionKey) {
750
- providerSessionState?.webSocketPublicToPrivate.set(publicSessionKey, sessionKey);
751
- }
902
+ const effectiveSessionKey =
903
+ providerSessionState && sessionKey
904
+ ? resolveCodexWebSocketSessionKey(sessionKey, publicSessionKey, providerSessionState)
905
+ : undefined;
906
+ logCodexDebug("build request context session keys", {
907
+ publicSessionKey,
908
+ effectiveSessionKey,
909
+ providerSessionStateMapIdentity: getProviderSessionStateIdentity(options?.providerSessionState),
910
+ });
752
911
  const websocketState =
753
- sessionKey && providerSessionState ? getCodexWebSocketSessionState(sessionKey, providerSessionState) : undefined;
912
+ providerSessionState && effectiveSessionKey
913
+ ? getCodexWebSocketSessionState(effectiveSessionKey, providerSessionState)
914
+ : undefined;
915
+ if (
916
+ websocketState &&
917
+ publicSessionKey &&
918
+ (disabledCodexPublicSessionKeys.has(publicSessionKey) ||
919
+ providerSessionState?.disabledWebSocketPublicSessions.has(publicSessionKey))
920
+ ) {
921
+ websocketState.disableWebsocket = true;
922
+ }
754
923
 
755
924
  return {
756
925
  apiKey,
@@ -1063,8 +1232,10 @@ function createCodexStreamRuntime(initial: {
1063
1232
  transport: initial.transport,
1064
1233
  websocketState: initial.websocketState,
1065
1234
  currentItem: null,
1235
+ currentItemOutputIndex: undefined,
1066
1236
  currentBlock: null,
1067
1237
  nativeOutputItems: [],
1238
+ nativeOutputItemOutputIndexes: [],
1068
1239
  websocketStreamRetries: 0,
1069
1240
  providerRetryAttempt: 0,
1070
1241
  toolChoiceFallbackAttempted: initial.toolChoiceFallbackApplied === true,
@@ -1077,6 +1248,7 @@ function createCodexStreamRuntime(initial: {
1077
1248
  canSafelyReplayWebsocketOverSse: true,
1078
1249
  finalizedToolCallIds: new Set<string>(),
1079
1250
  degradedIncrementDiagnostics: new Set<string>(),
1251
+ toolArgumentCorrelationFailed: false,
1080
1252
  };
1081
1253
  }
1082
1254
 
@@ -1086,10 +1258,10 @@ async function processCodexResponseStream(
1086
1258
  ): Promise<CodexStreamCompletion> {
1087
1259
  const { output, stream } = context;
1088
1260
  stream.push({ type: "start", partial: output });
1261
+ let firstTokenTime = context.firstTokenTime;
1089
1262
 
1090
1263
  while (true) {
1091
1264
  try {
1092
- let firstTokenTime = context.firstTokenTime;
1093
1265
  for await (const rawEvent of runtime.eventStream) {
1094
1266
  firstTokenTime = handleCodexStreamEvent({
1095
1267
  ...context,
@@ -1099,8 +1271,18 @@ async function processCodexResponseStream(
1099
1271
  });
1100
1272
  if (runtime.sawTerminalEvent) break;
1101
1273
  }
1274
+ if (!runtime.sawTerminalEvent) {
1275
+ trySalvageCodexFinalizedToolCalls(
1276
+ context,
1277
+ runtime,
1278
+ new Error("Codex stream ended before terminal completion event"),
1279
+ );
1280
+ }
1102
1281
  return { firstTokenTime };
1103
1282
  } catch (error) {
1283
+ if (trySalvageCodexFinalizedToolCalls(context, runtime, error)) {
1284
+ return { firstTokenTime };
1285
+ }
1104
1286
  const recovered = await recoverCodexStreamError(context, runtime, error);
1105
1287
  if (!recovered) {
1106
1288
  throw error;
@@ -1109,6 +1291,209 @@ async function processCodexResponseStream(
1109
1291
  }
1110
1292
  }
1111
1293
 
1294
+ function trySalvageCodexFinalizedToolCalls(
1295
+ context: CodexStreamProcessingContext,
1296
+ runtime: CodexStreamRuntime,
1297
+ error: unknown,
1298
+ ): boolean {
1299
+ const toolCalls = context.output.content.filter((block): block is ToolCall => block.type === "toolCall");
1300
+ const completeToolCalls = toolCalls.filter(toolCall => {
1301
+ const block = toolCall as ToolCall & {
1302
+ argumentsComplete?: boolean;
1303
+ argumentsAuthoritative?: boolean;
1304
+ sourceItemId?: string;
1305
+ sourceCallId?: string;
1306
+ };
1307
+ return (
1308
+ !runtime.finalizedToolCallIds.has(toolCall.id) &&
1309
+ block.argumentsComplete === true &&
1310
+ typeof block.sourceItemId === "string" &&
1311
+ typeof block.sourceCallId === "string" &&
1312
+ (block.argumentsAuthoritative === true || Object.keys(block.arguments).length > 0)
1313
+ );
1314
+ });
1315
+ const hasCompleteArguments = toolCalls.every(
1316
+ toolCall => runtime.finalizedToolCallIds.has(toolCall.id) || completeToolCalls.includes(toolCall),
1317
+ );
1318
+ const isIdleStall = isCodexIdleStall(error);
1319
+ const hasEmptyActiveMessage =
1320
+ runtime.currentItem?.type === "message" &&
1321
+ runtime.currentBlock?.type === "text" &&
1322
+ runtime.currentItem.content.every(content =>
1323
+ content.type === "output_text" ? content.text === "" : content.refusal === "",
1324
+ );
1325
+ // EOF proves a closed stream, but only finalized calls are safe without a timeout event.
1326
+ const isUnexpectedStreamEnd =
1327
+ error instanceof Error && error.message === "Codex stream ended before terminal completion event";
1328
+ const canSalvageFinalizedCall =
1329
+ (isCodexTransientStreamClose(error) || isUnexpectedStreamEnd) &&
1330
+ !runtime.toolArgumentCorrelationFailed &&
1331
+ toolCalls.length > 0 &&
1332
+ (runtime.currentBlock === null ||
1333
+ runtime.currentBlock?.type === "toolCall" ||
1334
+ (runtime.currentItem?.type === "reasoning" && runtime.currentBlock?.type === "thinking") ||
1335
+ hasEmptyActiveMessage) &&
1336
+ context.output.content.every(
1337
+ block =>
1338
+ block.type === "thinking" || block.type === "toolCall" || (block.type === "text" && block.text === ""),
1339
+ ) &&
1340
+ toolCalls.every(toolCall => runtime.finalizedToolCallIds.has(toolCall.id));
1341
+ const canSalvageCompleteArguments =
1342
+ (isCodexTransientStreamClose(error) || isIdleStall) &&
1343
+ !runtime.toolArgumentCorrelationFailed &&
1344
+ toolCalls.length > 0 &&
1345
+ (runtime.currentBlock === null ||
1346
+ runtime.currentBlock?.type === "toolCall" ||
1347
+ (runtime.currentItem?.type === "reasoning" && runtime.currentBlock?.type === "thinking") ||
1348
+ hasEmptyActiveMessage) &&
1349
+ context.output.content.every(
1350
+ block =>
1351
+ block.type === "thinking" || block.type === "toolCall" || (block.type === "text" && block.text === ""),
1352
+ ) &&
1353
+ hasCompleteArguments;
1354
+ if (!canSalvageFinalizedCall && !canSalvageCompleteArguments) {
1355
+ logCodexDebug("codex stream close salvage refused", {
1356
+ toolArgumentCorrelationFailed: runtime.toolArgumentCorrelationFailed,
1357
+ argumentsComplete: toolCalls.map(toolCall => {
1358
+ const block = toolCall as ToolCall & { argumentsComplete?: boolean };
1359
+ return block.argumentsComplete === true;
1360
+ }),
1361
+ sourceItemIdPresent: toolCalls.map(toolCall => {
1362
+ const block = toolCall as ToolCall & { sourceItemId?: string };
1363
+ return typeof block.sourceItemId === "string";
1364
+ }),
1365
+ sourceCallIdPresent: toolCalls.map(toolCall => {
1366
+ const block = toolCall as ToolCall & { sourceCallId?: string };
1367
+ return typeof block.sourceCallId === "string";
1368
+ }),
1369
+ currentItemType: runtime.currentItem?.type ?? null,
1370
+ currentBlockType: runtime.currentBlock?.type ?? null,
1371
+ });
1372
+ return false;
1373
+ }
1374
+
1375
+ if ((canSalvageFinalizedCall || canSalvageCompleteArguments) && runtime.currentBlock?.type === "thinking") {
1376
+ context.stream.push({
1377
+ type: "thinking_end",
1378
+ contentIndex: context.output.content.length - 1,
1379
+ content: runtime.currentBlock.thinking,
1380
+ partial: context.output,
1381
+ });
1382
+ runtime.currentItem = null;
1383
+ runtime.currentItemOutputIndex = undefined;
1384
+ runtime.currentBlock = null;
1385
+ }
1386
+
1387
+ if (canSalvageCompleteArguments) {
1388
+ for (const toolCallBlock of completeToolCalls) {
1389
+ const activeToolCall = toolCallBlock as ToolCall & {
1390
+ partialJson: string;
1391
+ argumentsComplete?: boolean;
1392
+ argumentsAuthoritative?: boolean;
1393
+ sourceItemId?: string;
1394
+ sourceCallId?: string;
1395
+ sourceName?: string;
1396
+ sourceOutputIndex?: number;
1397
+ };
1398
+ const item =
1399
+ runtime.currentBlock === activeToolCall && runtime.currentItem?.type === "function_call"
1400
+ ? runtime.currentItem
1401
+ : {
1402
+ type: "function_call",
1403
+ id: activeToolCall.sourceItemId,
1404
+ call_id: activeToolCall.sourceCallId,
1405
+ name: activeToolCall.sourceName ?? codexToolWireName(activeToolCall.name),
1406
+ arguments: activeToolCall.partialJson,
1407
+ };
1408
+ if (typeof item.id !== "string" || typeof item.call_id !== "string") continue;
1409
+ const toolCall: ToolCall = {
1410
+ type: "toolCall",
1411
+ id: activeToolCall.id,
1412
+ name: activeToolCall.name,
1413
+ arguments: activeToolCall.arguments,
1414
+ };
1415
+ const rawPartialJson = activeToolCall.partialJson;
1416
+ const sourceOutputIndex = activeToolCall.sourceOutputIndex;
1417
+ captureUnicodeEscapeEvidence(toolCall, rawPartialJson);
1418
+ Object.assign(activeToolCall, toolCall);
1419
+ captureUnicodeEscapeEvidence(activeToolCall, rawPartialJson);
1420
+ delete (activeToolCall as { partialJson?: string }).partialJson;
1421
+ delete (activeToolCall as { argumentsComplete?: boolean }).argumentsComplete;
1422
+ delete (activeToolCall as { argumentsAuthoritative?: boolean }).argumentsAuthoritative;
1423
+ delete (activeToolCall as { sourceItemId?: string }).sourceItemId;
1424
+ delete (activeToolCall as { sourceCallId?: string }).sourceCallId;
1425
+ delete (activeToolCall as { sourceName?: string }).sourceName;
1426
+ delete (activeToolCall as { sourceOutputIndex?: number }).sourceOutputIndex;
1427
+ runtime.finalizedToolCallIds.add(toolCall.id);
1428
+ const nativeOutputItem = {
1429
+ ...item,
1430
+ name:
1431
+ typeof item.name === "string"
1432
+ ? item.name
1433
+ : (activeToolCall.sourceName ?? codexToolWireName(activeToolCall.name)),
1434
+ arguments: JSON.stringify(toolCall.arguments),
1435
+ status: "completed",
1436
+ } as Record<string, unknown>;
1437
+ const insertionIndex =
1438
+ typeof sourceOutputIndex === "number"
1439
+ ? runtime.nativeOutputItemOutputIndexes.findIndex(
1440
+ outputIndex => typeof outputIndex === "number" && outputIndex > sourceOutputIndex,
1441
+ )
1442
+ : -1;
1443
+ const resolvedInsertionIndex = insertionIndex === -1 ? runtime.nativeOutputItems.length : insertionIndex;
1444
+ runtime.nativeOutputItems.splice(resolvedInsertionIndex, 0, nativeOutputItem);
1445
+ runtime.nativeOutputItemOutputIndexes.splice(resolvedInsertionIndex, 0, sourceOutputIndex);
1446
+ context.stream.push({
1447
+ type: "toolcall_end",
1448
+ contentIndex: context.output.content.indexOf(activeToolCall),
1449
+ toolCall,
1450
+ partial: context.output,
1451
+ });
1452
+ }
1453
+ runtime.currentItem = null;
1454
+ runtime.currentBlock = null;
1455
+ }
1456
+
1457
+ context.output.stopReason = "toolUse";
1458
+ context.output.errorCode = canSalvageFinalizedCall
1459
+ ? CODEX_SALVAGED_STREAM_CLOSE_ERROR_CODE
1460
+ : CODEX_SALVAGED_COMPLETE_TOOL_ARGUMENTS_ERROR_CODE;
1461
+ runtime.sawTerminalEvent = true;
1462
+ logCodexDebug("codex stream closed after complete tool arguments; salvaging tool-use response", {
1463
+ error: error instanceof Error ? error.message : String(error),
1464
+ toolCallCount: toolCalls.length,
1465
+ transport: runtime.transport,
1466
+ });
1467
+ if (context.requestContext.websocketState) {
1468
+ resetCodexWebSocketAppendState(context.requestContext.websocketState);
1469
+ }
1470
+ return true;
1471
+ }
1472
+
1473
+ function isCodexTransientStreamClose(error: unknown): boolean {
1474
+ if (!(error instanceof Error)) return false;
1475
+ const providerMessage = (error as CodexProviderStreamError).providerMessage;
1476
+ const message = (providerMessage || error.message).toLowerCase();
1477
+ const providerCode =
1478
+ (error as CodexProviderStreamError & { providerCode?: string }).code?.toLowerCase() ??
1479
+ (error as { providerCode?: string }).providerCode?.toLowerCase();
1480
+ if (providerCode && CODEX_NON_RETRYABLE_EVENT_CODES.has(providerCode)) return false;
1481
+ const hasRequestTimeout = providerCode === "request_timeout" || message.includes("request_timeout");
1482
+ const hasClosedStreamMessage =
1483
+ message.includes("stream disconnected before completion") ||
1484
+ message.includes("stream closed before response.completed") ||
1485
+ message.includes("websocket closed before response completion");
1486
+ return (hasRequestTimeout && hasClosedStreamMessage) || isCodexIdleStall(error);
1487
+ }
1488
+
1489
+ function isCodexIdleStall(error: unknown): boolean {
1490
+ if (!(error instanceof Error)) return false;
1491
+ return (
1492
+ error.message === "OpenAI Codex SSE stream stalled while waiting for the next event" ||
1493
+ error.message === `${CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX}: idle timeout waiting for websocket`
1494
+ );
1495
+ }
1496
+
1112
1497
  function handleCodexStreamEvent(args: {
1113
1498
  model: Model<"openai-codex-responses">;
1114
1499
  output: AssistantMessage;
@@ -1129,8 +1514,12 @@ function handleCodexStreamEvent(args: {
1129
1514
  if (!firstTokenTime) firstTokenTime = Date.now();
1130
1515
  const item = rawEvent.item as CodexEventItem;
1131
1516
  runtime.currentItem = item;
1517
+ runtime.currentItemOutputIndex = typeof rawEvent.output_index === "number" ? rawEvent.output_index : undefined;
1132
1518
  runtime.currentBlock = createOutputBlockForItem(item);
1133
1519
  if (!runtime.currentBlock) return firstTokenTime;
1520
+ if (runtime.currentBlock.type === "toolCall" && typeof runtime.currentItemOutputIndex === "number") {
1521
+ runtime.currentBlock.sourceOutputIndex = runtime.currentItemOutputIndex;
1522
+ }
1134
1523
  const currentBlock = runtime.currentBlock;
1135
1524
  if (
1136
1525
  currentBlock.type === "toolCall" &&
@@ -1215,12 +1604,20 @@ function handleCodexStreamEvent(args: {
1215
1604
  }
1216
1605
 
1217
1606
  if (eventType === "response.function_call_arguments.delta") {
1607
+ if (!isActiveToolArgumentEvent(runtime, rawEvent)) {
1608
+ runtime.toolArgumentCorrelationFailed = true;
1609
+ return firstTokenTime;
1610
+ }
1218
1611
  const delta = assertStringToolArgumentIncrement(rawEvent, "response.function_call_arguments.delta");
1219
1612
  handleToolCallArgumentsDelta(runtime.currentItem, runtime.currentBlock, delta, stream, output, blockIndex);
1220
1613
  return firstTokenTime;
1221
1614
  }
1222
1615
 
1223
1616
  if (eventType === "response.function_call_arguments.done") {
1617
+ if (!isActiveToolArgumentEvent(runtime, rawEvent)) {
1618
+ runtime.toolArgumentCorrelationFailed = true;
1619
+ return firstTokenTime;
1620
+ }
1224
1621
  handleToolCallArgumentsDone(runtime.currentItem, runtime.currentBlock, rawEvent);
1225
1622
  return firstTokenTime;
1226
1623
  }
@@ -1265,13 +1662,38 @@ function createOutputBlockForItem(item: CodexEventItem): CodexOutputBlock | null
1265
1662
  return { type: "text", text: "" };
1266
1663
  }
1267
1664
  if (item.type === "function_call") {
1268
- return {
1665
+ const initialArguments = item.arguments || "";
1666
+ let parsedArguments: Record<string, unknown> = parseStreamingJson(initialArguments);
1667
+ try {
1668
+ const parsed = JSON.parse(initialArguments);
1669
+ if (isPlainJsonObject(parsed)) parsedArguments = parsed;
1670
+ } catch {
1671
+ // Keep the streaming parser's partial object for deltas, but do not
1672
+ // treat an incomplete snapshot as executable arguments.
1673
+ }
1674
+ const hasValidInitialArguments = isCompleteJsonObject(initialArguments);
1675
+ const block: ToolCall & {
1676
+ partialJson: string;
1677
+ argumentsComplete: boolean;
1678
+ argumentsAuthoritative?: boolean;
1679
+ sourceItemId?: string;
1680
+ sourceCallId?: string;
1681
+ sourceName?: string;
1682
+ sourceOutputIndex?: number;
1683
+ } = {
1269
1684
  type: "toolCall",
1270
1685
  id: encodeResponsesToolCallId(item.call_id, item.id),
1271
1686
  name: codexToolCanonicalName(item.name),
1272
- arguments: {},
1273
- partialJson: item.arguments || "",
1687
+ arguments: hasValidInitialArguments ? parsedArguments : {},
1688
+ partialJson: initialArguments,
1689
+ argumentsComplete: hasValidInitialArguments && Object.keys(parsedArguments).length > 0,
1690
+ argumentsAuthoritative: false,
1691
+ sourceItemId: item.id,
1692
+ sourceCallId: item.call_id,
1693
+ sourceName: item.name,
1274
1694
  };
1695
+ captureUnicodeEscapeEvidence(block, initialArguments);
1696
+ return block;
1275
1697
  }
1276
1698
  if (item.type === "custom_tool_call") {
1277
1699
  const initialInput: unknown = item.input;
@@ -1440,8 +1862,14 @@ function handleToolCallArgumentsDelta(
1440
1862
  blockIndex: () => number,
1441
1863
  ): void {
1442
1864
  if (currentItem?.type !== "function_call" || currentBlock?.type !== "toolCall") return;
1865
+ if (currentBlock.argumentsComplete === true && delta.trim().length > 0) {
1866
+ currentBlock.argumentsComplete = false;
1867
+ }
1443
1868
  currentBlock.partialJson += delta;
1444
1869
  currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
1870
+ currentBlock.argumentsComplete = isCompleteJsonObject(currentBlock.partialJson);
1871
+ currentBlock.argumentsAuthoritative =
1872
+ currentBlock.argumentsAuthoritative || (currentBlock.argumentsComplete && delta.trim().length > 0);
1445
1873
  stream.push({ type: "toolcall_delta", contentIndex: blockIndex(), delta, partial: output });
1446
1874
  }
1447
1875
 
@@ -1455,10 +1883,25 @@ function handleToolCallArgumentsDone(
1455
1883
  if (typeof args === "string") {
1456
1884
  currentBlock.partialJson = args;
1457
1885
  currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
1886
+ currentBlock.argumentsComplete = isCompleteJsonObject(currentBlock.partialJson);
1887
+ currentBlock.argumentsAuthoritative = currentBlock.argumentsComplete;
1458
1888
  captureUnicodeEscapeEvidence(currentBlock, args);
1459
1889
  }
1460
1890
  }
1461
1891
 
1892
+ function isActiveToolArgumentEvent(runtime: CodexStreamRuntime, rawEvent: Record<string, unknown>): boolean {
1893
+ if (runtime.currentItem?.type !== "function_call" || runtime.currentBlock?.type !== "toolCall") return false;
1894
+ const itemId = rawEvent.item_id;
1895
+ if (typeof itemId === "string" && itemId !== runtime.currentItem.id) return false;
1896
+ const callId = rawEvent.call_id;
1897
+ if (typeof callId === "string" && callId !== runtime.currentItem.call_id) return false;
1898
+ const outputIndex = rawEvent.output_index;
1899
+ if (typeof outputIndex === "number" && outputIndex !== runtime.currentItemOutputIndex) {
1900
+ return false;
1901
+ }
1902
+ return true;
1903
+ }
1904
+
1462
1905
  function handleCustomToolCallInputDelta(
1463
1906
  currentItem: CodexEventItem | null,
1464
1907
  currentBlock: CodexOutputBlock | null,
@@ -1502,6 +1945,9 @@ function handleOutputItemDone(
1502
1945
  ): void {
1503
1946
  const item = structuredCloneJSON(rawEvent.item) as CodexEventItem;
1504
1947
  runtime.nativeOutputItems.push(item as unknown as Record<string, unknown>);
1948
+ runtime.nativeOutputItemOutputIndexes.push(
1949
+ typeof rawEvent.output_index === "number" ? rawEvent.output_index : undefined,
1950
+ );
1505
1951
 
1506
1952
  if (item.type === "reasoning" && runtime.currentBlock?.type === "thinking") {
1507
1953
  const block = runtime.currentBlock;
@@ -1598,7 +2044,13 @@ function handleOutputItemDone(
1598
2044
  Object.assign(runtime.currentBlock, toolCall);
1599
2045
  captureUnicodeEscapeEvidence(runtime.currentBlock, item.arguments);
1600
2046
  delete (runtime.currentBlock as { partialJson?: string }).partialJson;
2047
+ delete (runtime.currentBlock as { argumentsComplete?: boolean }).argumentsComplete;
2048
+ delete (runtime.currentBlock as { argumentsAuthoritative?: boolean }).argumentsAuthoritative;
1601
2049
  delete (runtime.currentBlock as { doneInput?: string }).doneInput;
2050
+ delete (runtime.currentBlock as { sourceItemId?: string }).sourceItemId;
2051
+ delete (runtime.currentBlock as { sourceCallId?: string }).sourceCallId;
2052
+ delete (runtime.currentBlock as { sourceName?: string }).sourceName;
2053
+ delete (runtime.currentBlock as { sourceOutputIndex?: number }).sourceOutputIndex;
1602
2054
  runtime.canSafelyReplayWebsocketOverSse = false;
1603
2055
  stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
1604
2056
  runtime.currentItem = null;
@@ -1738,6 +2190,7 @@ async function recoverCodexStreamError(
1738
2190
  if (isCodexFirstEventTimeout(error)) return false;
1739
2191
  // A post-progress idle timeout has already consumed the stream's silence budget.
1740
2192
  // Replaying it over another transport can leave the turn silent for another full window.
2193
+ // A tool-call start is progress even without reasoning, usage, or output_item.done.
1741
2194
  if (
1742
2195
  (codexOutputHasMeaningfulProgress(context.output) ||
1743
2196
  (context.output.content.some(block => block.type === "thinking") &&
@@ -1804,7 +2257,9 @@ async function tryRetryWithoutForcedToolChoice(
1804
2257
  runtime.currentBlock = null;
1805
2258
  runtime.sawTerminalEvent = false;
1806
2259
  runtime.nativeOutputItems.length = 0;
2260
+ runtime.nativeOutputItemOutputIndexes.length = 0;
1807
2261
  runtime.finalizedToolCallIds.clear();
2262
+ runtime.toolArgumentCorrelationFailed = false;
1808
2263
  resetOutputState(context.output);
1809
2264
  context.firstTokenTime = undefined;
1810
2265
 
@@ -1902,7 +2357,9 @@ async function tryReconnectCodexWebSocketOnConnectionLimit(
1902
2357
  runtime.currentItem = null;
1903
2358
  runtime.currentBlock = null;
1904
2359
  runtime.nativeOutputItems.length = 0;
2360
+ runtime.nativeOutputItemOutputIndexes.length = 0;
1905
2361
  runtime.finalizedToolCallIds.clear();
2362
+ runtime.toolArgumentCorrelationFailed = false;
1906
2363
  resetOutputState(context.output);
1907
2364
  context.firstTokenTime = undefined;
1908
2365
  recordCodexWebSocketFailure(websocketState, true);
@@ -1940,14 +2397,14 @@ async function tryRecoverCodexPreviousResponseNotFound(
1940
2397
  // validation fault: clearing session metadata and resending the identical
1941
2398
  // body cannot fix it.
1942
2399
  !runtime.sentPreviousResponseId ||
2400
+ // Managed fallback forces streamMaxRetries to 0; this replay is exempt
2401
+ // from that budget and bounded by previousResponseRecoveryAttempted.
1943
2402
  runtime.previousResponseRecoveryAttempted ||
1944
2403
  !websocketState ||
1945
- context.options?.fallbackManaged ||
1946
2404
  context.options?.disableProviderRetries ||
1947
2405
  runtime.transport !== "websocket" ||
1948
2406
  context.output.content.length > 0 ||
1949
- context.options?.signal?.aborted ||
1950
- runtime.providerRetryAttempt >= resolveRetryBudget(context.options?.streamMaxRetries, CODEX_MAX_RETRIES)
2407
+ context.options?.signal?.aborted
1951
2408
  ) {
1952
2409
  return false;
1953
2410
  }
@@ -1960,7 +2417,9 @@ async function tryRecoverCodexPreviousResponseNotFound(
1960
2417
  runtime.currentBlock = null;
1961
2418
  runtime.sawTerminalEvent = false;
1962
2419
  runtime.nativeOutputItems.length = 0;
2420
+ runtime.nativeOutputItemOutputIndexes.length = 0;
1963
2421
  runtime.finalizedToolCallIds.clear();
2422
+ runtime.toolArgumentCorrelationFailed = false;
1964
2423
  resetOutputState(context.output);
1965
2424
  context.firstTokenTime = undefined;
1966
2425
 
@@ -2020,7 +2479,9 @@ async function tryReplayWebsocketFailureOverSse(
2020
2479
  runtime.currentItem = null;
2021
2480
  runtime.currentBlock = null;
2022
2481
  runtime.nativeOutputItems.length = 0;
2482
+ runtime.nativeOutputItemOutputIndexes.length = 0;
2023
2483
  runtime.finalizedToolCallIds.clear();
2484
+ runtime.toolArgumentCorrelationFailed = false;
2024
2485
  resetOutputState(context.output);
2025
2486
  context.firstTokenTime = undefined;
2026
2487
  }
@@ -2063,7 +2524,9 @@ async function tryRetryCodexProviderError(
2063
2524
  runtime.currentBlock = null;
2064
2525
  runtime.sawTerminalEvent = false;
2065
2526
  runtime.nativeOutputItems.length = 0;
2527
+ runtime.nativeOutputItemOutputIndexes.length = 0;
2066
2528
  runtime.finalizedToolCallIds.clear();
2529
+ runtime.toolArgumentCorrelationFailed = false;
2067
2530
  resetOutputState(context.output);
2068
2531
  context.firstTokenTime = undefined;
2069
2532
  await scheduler.wait(CODEX_RETRY_DELAY_MS * runtime.providerRetryAttempt, {
@@ -2127,7 +2590,25 @@ async function handleCodexStreamFailure(
2127
2590
  }
2128
2591
  output.stopReason = context.options?.signal?.aborted ? "aborted" : "error";
2129
2592
  output.errorStatus = extractHttpStatusFromError(error);
2130
- output.transportFailure = transportFailureFacts(error);
2593
+ const transportFailure = transportFailureFacts(error);
2594
+ const codexError = error instanceof CodexProviderStreamError ? error : undefined;
2595
+ const explicitTerminalVeto =
2596
+ codexError !== undefined && isExplicitCodexTerminalVeto(codexError.code, codexError.providerMessage);
2597
+ const typedProviderCode =
2598
+ codexError !== undefined && CODEX_TYPED_TRANSPORT_PROVIDER_CODES.has(codexError.code ?? "")
2599
+ ? codexError.code
2600
+ : undefined;
2601
+ output.transportFailure = typedProviderCode
2602
+ ? explicitTerminalVeto
2603
+ ? undefined
2604
+ : {
2605
+ ...(transportFailure ?? { kind: "transport" as const }),
2606
+ providerCode: typedProviderCode,
2607
+ ...(codexError?.deterministicVeto ? { retryMaxAttempts: 1 } : {}),
2608
+ }
2609
+ : explicitTerminalVeto
2610
+ ? undefined
2611
+ : transportFailure;
2131
2612
  output.errorMessage = await finalizeErrorMessage(error, context.requestContext.rawRequestDump);
2132
2613
  output.duration = Date.now() - context.startTime;
2133
2614
  if (context.firstTokenTime) {
@@ -2245,11 +2726,14 @@ export async function prewarmOpenAICodexResponses(
2245
2726
  const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
2246
2727
  const sessionKey = getCodexWebSocketSessionKey(promptCacheKey, model, accountId, baseUrl);
2247
2728
  const publicSessionKey = getCodexPublicSessionKey(promptCacheKey, model, baseUrl);
2248
- if (publicSessionKey && sessionKey) {
2249
- providerSessionState?.webSocketPublicToPrivate.set(publicSessionKey, sessionKey);
2250
- }
2251
2729
  if (!sessionKey || !providerSessionState) return;
2252
- const state = getCodexWebSocketSessionState(sessionKey, providerSessionState);
2730
+ const effectiveSessionKey = resolveCodexWebSocketSessionKey(sessionKey, publicSessionKey, providerSessionState);
2731
+ logCodexDebug("prewarm session keys", {
2732
+ publicSessionKey,
2733
+ effectiveSessionKey,
2734
+ providerSessionStateMapIdentity: getProviderSessionStateIdentity(options?.providerSessionState),
2735
+ });
2736
+ const state = getCodexWebSocketSessionState(effectiveSessionKey, providerSessionState);
2253
2737
  if (!shouldUseCodexWebSocket(model, state, options?.preferWebsockets)) return;
2254
2738
  const headers = logger.time(
2255
2739
  "prewarmCodex:createHeaders",
@@ -2261,7 +2745,7 @@ export async function prewarmOpenAICodexResponses(
2261
2745
  "websocket",
2262
2746
  state,
2263
2747
  );
2264
- await logger.time(
2748
+ const connectionPromise = logger.time(
2265
2749
  "prewarmCodex:establishWs",
2266
2750
  getOrCreateCodexWebSocketConnection,
2267
2751
  state,
@@ -2269,7 +2753,35 @@ export async function prewarmOpenAICodexResponses(
2269
2753
  headers,
2270
2754
  options?.signal,
2271
2755
  );
2272
- state.prewarmed = true;
2756
+ const attemptedConnection = state.connection;
2757
+ try {
2758
+ await connectionPromise;
2759
+ } catch (error) {
2760
+ const websocketError = error instanceof Error ? error : new Error(String(error));
2761
+ const superseded =
2762
+ attemptedConnection?.wasClosedLocally() === true ||
2763
+ (attemptedConnection !== undefined && state.connection !== attemptedConnection);
2764
+ const fatalPrewarmFailure =
2765
+ isCodexWebSocketFatalError(websocketError) &&
2766
+ !websocketError.message.toLowerCase().includes("connection timeout") &&
2767
+ !/^.*websocket closed before open \((?:1000|1001|1005)\)$/i.test(websocketError.message) &&
2768
+ !superseded &&
2769
+ state.connection === attemptedConnection;
2770
+ if (fatalPrewarmFailure) {
2771
+ recordCodexWebSocketFailure(state, true);
2772
+ if (publicSessionKey) providerSessionState.disabledWebSocketPublicSessions.add(publicSessionKey);
2773
+ if (publicSessionKey) disabledCodexPublicSessionKeys.add(publicSessionKey);
2774
+ } else if (superseded) {
2775
+ logCodexDebug("ignoring superseded Codex websocket prewarm failure", {
2776
+ error: websocketError.message,
2777
+ publicSessionKey,
2778
+ });
2779
+ }
2780
+ throw error;
2781
+ }
2782
+ if (state.connection === attemptedConnection) {
2783
+ state.prewarmed = true;
2784
+ }
2273
2785
  }
2274
2786
 
2275
2787
  function getCodexWebSocketSessionKey(
@@ -2283,6 +2795,17 @@ function getCodexWebSocketSessionKey(
2283
2795
  return `${accountId ?? "opaque"}:${baseUrl}:${model.id}:${promptCacheKey}`;
2284
2796
  }
2285
2797
 
2798
+ function resolveCodexWebSocketSessionKey(
2799
+ sessionKey: string,
2800
+ publicSessionKey: string | undefined,
2801
+ providerSessionState: CodexProviderSessionState,
2802
+ ): string {
2803
+ if (publicSessionKey) {
2804
+ providerSessionState.webSocketPublicToPrivate.set(publicSessionKey, sessionKey);
2805
+ }
2806
+ return sessionKey;
2807
+ }
2808
+
2286
2809
  function getCodexPublicSessionKey(
2287
2810
  sessionId: string | undefined,
2288
2811
  model: Model<"openai-codex-responses">,
@@ -2548,6 +3071,7 @@ class CodexWebSocketConnection {
2548
3071
  #waiters: Array<() => void> = [];
2549
3072
  #connectPromise?: Promise<void>;
2550
3073
  #activeRequest = false;
3074
+ #localCloseReason?: string;
2551
3075
 
2552
3076
  constructor(url: string, headers: Record<string, string>, options: CodexWebSocketConnectionOptions) {
2553
3077
  this.#url = url;
@@ -2560,11 +3084,16 @@ class CodexWebSocketConnection {
2560
3084
  return this.#socket?.readyState === WebSocket.OPEN;
2561
3085
  }
2562
3086
 
3087
+ wasClosedLocally(): boolean {
3088
+ return this.#localCloseReason !== undefined;
3089
+ }
3090
+
2563
3091
  matchesAuth(headers: Record<string, string>): boolean {
2564
3092
  return this.#headers.authorization === headers.authorization;
2565
3093
  }
2566
3094
 
2567
3095
  close(reason = "done"): void {
3096
+ this.#localCloseReason = reason;
2568
3097
  if (
2569
3098
  this.#socket &&
2570
3099
  (this.#socket.readyState === WebSocket.OPEN || this.#socket.readyState === WebSocket.CONNECTING)
@@ -2790,14 +3319,17 @@ class CodexWebSocketConnection {
2790
3319
  let timedOut = false;
2791
3320
  let timeout: NodeJS.Timeout | undefined;
2792
3321
  if (timeoutMs !== undefined && timeoutMs > 0) {
2793
- timeout = setTimeout(() => {
2794
- timedOut = true;
2795
- const waiterIndex = this.#waiters.indexOf(resolve);
2796
- if (waiterIndex >= 0) {
2797
- this.#waiters.splice(waiterIndex, 1);
2798
- }
2799
- resolve();
2800
- }, timeoutMs);
3322
+ timeout = setTimeout(
3323
+ () => {
3324
+ timedOut = true;
3325
+ const waiterIndex = this.#waiters.indexOf(resolve);
3326
+ if (waiterIndex >= 0) {
3327
+ this.#waiters.splice(waiterIndex, 1);
3328
+ }
3329
+ resolve();
3330
+ },
3331
+ Math.min(timeoutMs, CODEX_MAX_TIMER_DELAY_MS),
3332
+ );
2801
3333
  }
2802
3334
  await promise;
2803
3335
  if (timeout) clearTimeout(timeout);
@@ -3205,7 +3737,7 @@ export function convertOpenAICodexResponsesTools(
3205
3737
  name: codexToolWireName(tool.name),
3206
3738
  description: tool.description || "",
3207
3739
  parameters,
3208
- ...(effectiveStrict && { strict: true }),
3740
+ strict: effectiveStrict,
3209
3741
  };
3210
3742
  });
3211
3743
  // Tool definitions bypass the `input`/`instructions` sanitizers, so a
@@ -3236,6 +3768,7 @@ function getCodexEventErrorMessage(rawEvent: Record<string, unknown>): string {
3236
3768
 
3237
3769
  class CodexProviderStreamError extends Error {
3238
3770
  readonly retryable: boolean;
3771
+ readonly deterministicVeto: boolean;
3239
3772
  readonly code?: string;
3240
3773
  readonly credentialModelUnavailable: boolean;
3241
3774
  /**
@@ -3249,6 +3782,7 @@ class CodexProviderStreamError extends Error {
3249
3782
  constructor(
3250
3783
  message: string,
3251
3784
  retryable: boolean,
3785
+ deterministicVeto: boolean,
3252
3786
  code: string | undefined,
3253
3787
  providerMessage: string,
3254
3788
  credentialModelUnavailable: boolean,
@@ -3256,6 +3790,7 @@ class CodexProviderStreamError extends Error {
3256
3790
  super(message);
3257
3791
  this.name = "CodexProviderStreamError";
3258
3792
  this.retryable = retryable;
3793
+ this.deterministicVeto = deterministicVeto;
3259
3794
  this.code = code;
3260
3795
  this.providerMessage = providerMessage;
3261
3796
  this.credentialModelUnavailable = credentialModelUnavailable;
@@ -3265,10 +3800,7 @@ class CodexProviderStreamError extends Error {
3265
3800
  function isRetryableCodexFailureEvent(rawEvent: Record<string, unknown>): boolean {
3266
3801
  const code = getCodexEventErrorCode(rawEvent).toLowerCase();
3267
3802
  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
- ) {
3803
+ if (isCodexDeterministicVeto(code, message)) {
3272
3804
  return false;
3273
3805
  }
3274
3806
  if (code && CODEX_RETRYABLE_EVENT_CODES.has(code)) {
@@ -3277,9 +3809,24 @@ function isRetryableCodexFailureEvent(rawEvent: Record<string, unknown>): boolea
3277
3809
  return !!message && CODEX_RETRYABLE_EVENT_MESSAGE.test(message);
3278
3810
  }
3279
3811
 
3812
+ function isCodexDeterministicVeto(code: string, message: string): boolean {
3813
+ return (
3814
+ (code && CODEX_NON_RETRYABLE_EVENT_CODES.has(code)) ||
3815
+ (!!message && CODEX_NON_RETRYABLE_EVENT_MESSAGE.test(message))
3816
+ );
3817
+ }
3818
+
3819
+ export function isExplicitCodexTerminalVeto(code?: string, providerMessage?: string): boolean {
3820
+ return (
3821
+ CODEX_EXPLICIT_TERMINAL_VETO_CODES.has(code?.toLowerCase() ?? "") ||
3822
+ CODEX_NON_RETRYABLE_EVENT_MESSAGE.test(providerMessage ?? "")
3823
+ );
3824
+ }
3825
+
3280
3826
  function createCodexProviderStreamError(rawEvent: Record<string, unknown>): CodexProviderStreamError {
3281
3827
  const code = getCodexEventErrorCode(rawEvent);
3282
3828
  const message = getCodexEventErrorMessage(rawEvent);
3829
+ const deterministicVeto = isCodexDeterministicVeto(code.toLowerCase(), message);
3283
3830
  const formattedMessage =
3284
3831
  typeof rawEvent.type === "string" && rawEvent.type === "error"
3285
3832
  ? formatCodexErrorEvent(rawEvent, code, message)
@@ -3287,6 +3834,7 @@ function createCodexProviderStreamError(rawEvent: Record<string, unknown>): Code
3287
3834
  return new CodexProviderStreamError(
3288
3835
  formattedMessage,
3289
3836
  isRetryableCodexFailureEvent(rawEvent),
3837
+ deterministicVeto,
3290
3838
  code || undefined,
3291
3839
  message,
3292
3840
  isCodexAccountModelUnavailable(message, code),