@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.
- package/CHANGELOG.md +73 -0
- package/dist/types/auth-storage.d.ts +8 -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 +3 -0
- package/dist/types/types.d.ts +16 -1
- package/dist/types/utils/fallback-transport.d.ts +6 -0
- package/dist/types/utils/idle-iterator.d.ts +2 -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/model-thinking.ts +8 -0
- 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/google-gemini-cli.ts +49 -3
- 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 +164 -68
- package/src/providers/openai-codex-responses.d.ts +3 -0
- package/src/providers/openai-codex-responses.ts +589 -41
- package/src/providers/openai-opencodex-responses.ts +6 -1
- package/src/providers/openai-responses.ts +1 -1
- package/src/providers/register-builtins.ts +53 -5
- package/src/stream.ts +20 -0
- package/src/types.d.ts +16 -1
- package/src/types.ts +19 -0
- package/src/utils/fallback-transport.d.ts +6 -0
- package/src/utils/fallback-transport.ts +6 -0
- package/src/utils/idle-iterator.d.ts +2 -0
- package/src/utils/idle-iterator.ts +13 -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
|
@@ -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",
|
|
@@ -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 = [
|
|
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
|
|
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) ||
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
750
|
-
providerSessionState
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
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
|
-
|
|
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),
|