@gajae-code/ai 0.14.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/types/auth-storage.d.ts +2 -2
- package/dist/types/model-cache.d.ts +2 -0
- package/dist/types/provider-models/special.d.ts +3 -1
- package/dist/types/providers/anthropic.d.ts +1 -0
- package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
- package/dist/types/providers/cursor/gen/agent_pb.d.ts +3854 -107
- package/dist/types/providers/cursor-pi-args.d.ts +119 -0
- package/dist/types/providers/cursor.d.ts +8 -1
- package/dist/types/providers/openai-codex-responses.d.ts +2 -0
- package/dist/types/providers/openai-responses-shared.d.ts +1 -1
- package/dist/types/types.d.ts +41 -1
- package/dist/types/utils/block-symbols.d.ts +6 -0
- package/dist/types/utils/discovery/openai-compatible.d.ts +2 -0
- package/dist/types/utils/idle-iterator.d.ts +5 -2
- package/dist/types/utils/oauth/kimi.d.ts +3 -9
- package/dist/types/utils/oauth/openrouter.d.ts +1 -0
- package/dist/types/utils/oauth/types.d.ts +1 -1
- package/package.json +4 -4
- package/src/auth-broker/remote-store.ts +13 -2
- package/src/auth-storage.ts +10 -11
- package/src/model-cache.ts +78 -0
- package/src/model-manager.ts +194 -25
- package/src/provider-models/special.ts +67 -4
- package/src/providers/anthropic.ts +115 -40
- package/src/providers/aws-credential-config.ts +2 -3
- package/src/providers/aws-credentials.ts +2 -3
- package/src/providers/azure-openai-responses.ts +18 -2
- package/src/providers/cursor/exec-modern.ts +497 -0
- package/src/providers/cursor/gen/agent_pb.ts +4687 -181
- package/src/providers/cursor/proto/agent.proto +1007 -0
- package/src/providers/cursor-pi-args.ts +187 -0
- package/src/providers/cursor.ts +382 -47
- package/src/providers/google-auth.ts +2 -3
- package/src/providers/openai-codex-responses.ts +358 -73
- package/src/providers/openai-completions.ts +2 -2
- package/src/providers/openai-responses-shared.ts +55 -6
- package/src/providers/openai-responses.ts +27 -4
- package/src/stream.ts +8 -3
- package/src/types.ts +55 -0
- package/src/utils/block-symbols.ts +11 -0
- package/src/utils/discovery/openai-compatible.ts +21 -6
- package/src/utils/idle-iterator.ts +22 -4
- package/src/utils/oauth/index.ts +6 -0
- package/src/utils/oauth/kimi.ts +14 -8
- package/src/utils/oauth/kiro.ts +2 -2
- package/src/utils/oauth/openrouter.ts +16 -0
- package/src/utils/oauth/types.ts +1 -0
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
fetchWithRetry,
|
|
9
9
|
logger,
|
|
10
10
|
readSseJson,
|
|
11
|
+
sanitizeHeaderComponent,
|
|
11
12
|
structuredCloneJSON,
|
|
12
13
|
} from "@gajae-code/utils";
|
|
13
14
|
import type OpenAI from "openai";
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
type Tool,
|
|
40
41
|
type ToolCall,
|
|
41
42
|
type ToolChoice,
|
|
43
|
+
type ToolResultMessage,
|
|
42
44
|
} from "../types";
|
|
43
45
|
import {
|
|
44
46
|
createOpenAIResponsesHistoryPayload,
|
|
@@ -115,6 +117,46 @@ const CODEX_WEBSOCKET_IDLE_TIMEOUT_MS = 300000;
|
|
|
115
117
|
const CODEX_WEBSOCKET_RETRY_BUDGET = CODEX_MAX_RETRIES;
|
|
116
118
|
const CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX = "Codex websocket transport error";
|
|
117
119
|
const CODEX_PREVIOUS_RESPONSE_STALE_CODES = new Set(["previous_response_not_found", "codex_previous_response_stale"]);
|
|
120
|
+
// Some Codex deployments reject a stale continuation anchor with a generic
|
|
121
|
+
// `invalid_request_error` code and name the anchor only in the message
|
|
122
|
+
// (`Invalid \`previous_response_id\`.`). Match the canonical request-field token
|
|
123
|
+
// with a stale qualifier on either side, so the anchor is cleared and the turn
|
|
124
|
+
// retried with full context instead of killing the session.
|
|
125
|
+
//
|
|
126
|
+
// The compact form deliberately requires the `previous_response_id` field token
|
|
127
|
+
// and is unguarded: a message naming the field itself is about the anchor. Prose
|
|
128
|
+
// references are guarded separately below — the division of labor is compact
|
|
129
|
+
// token (unguarded, field-naming) vs prose phrase (guarded, fault-noun checked
|
|
130
|
+
// on both sides of the anchor phrase), since a deterministic history fault such
|
|
131
|
+
// as `Previous response's tool call ID is malformed.` must stay fatal:
|
|
132
|
+
// replaying it re-sends the same offending item.
|
|
133
|
+
const CODEX_PREVIOUS_RESPONSE_ID_TOKEN = String.raw`previous[ _-]response[ _-]id`;
|
|
134
|
+
// Prose anchor reference ("Previous response with id 'resp_1' not found."):
|
|
135
|
+
// the canonical `previous_response_not_found` wording, which codex-lb re-codes
|
|
136
|
+
// as `invalid_request_error` the same way it re-codes the anchor-expiry fault
|
|
137
|
+
// to `codex_previous_response_stale`. The compact token above cannot match it,
|
|
138
|
+
// so the recovery path missed these and the session died.
|
|
139
|
+
const CODEX_PREVIOUS_RESPONSE_PROSE_TOKEN = `previous[ _-]response`;
|
|
140
|
+
// Sub-field faults INSIDE the previous response (tool call / call id / message
|
|
141
|
+
// id / item) are deterministic history faults: replaying full context re-sends
|
|
142
|
+
// the same offending item. Tempering the qualifier⇄token scan against these
|
|
143
|
+
// tokens keeps them fatal while pure anchor-stale prose still matches.
|
|
144
|
+
const CODEX_PREVIOUS_RESPONSE_STALE_SUBFIELD_GUARD = `tool[ _]calls?|function[ _]calls?|custom[ _]tools?|call[ _-]?ids?|message[ _]?ids?|items?\\b|output[ _-]?items?`;
|
|
145
|
+
const CODEX_ANCHOR_STALE_QUALIFIER = String.raw`invalid|expired|unknown|stale|not[ _-]?found|no longer`;
|
|
146
|
+
const CODEX_PREVIOUS_RESPONSE_STALE_MESSAGE = new RegExp(
|
|
147
|
+
`(?:${CODEX_ANCHOR_STALE_QUALIFIER})[^\\n]{0,48}?${CODEX_PREVIOUS_RESPONSE_ID_TOKEN}` +
|
|
148
|
+
`|${CODEX_PREVIOUS_RESPONSE_ID_TOKEN}[^\\n]{0,48}?(?:${CODEX_ANCHOR_STALE_QUALIFIER})`,
|
|
149
|
+
"i",
|
|
150
|
+
);
|
|
151
|
+
const CODEX_PREVIOUS_RESPONSE_STALE_PROSE_MESSAGE = new RegExp(
|
|
152
|
+
// The fault noun can sit on EITHER side of the anchor phrase AND on either
|
|
153
|
+
// side of the qualifier (`Unknown previous response tool call.`,
|
|
154
|
+
// `Previous response includes an unknown tool call.`), so every alternative
|
|
155
|
+
// carries both the tempered inter-token scan and a post-anchor lookahead.
|
|
156
|
+
`(?:${CODEX_ANCHOR_STALE_QUALIFIER})(?:(?!${CODEX_PREVIOUS_RESPONSE_STALE_SUBFIELD_GUARD})[^\\n]){0,48}?${CODEX_PREVIOUS_RESPONSE_PROSE_TOKEN}(?![^\\n]{0,48}(?:${CODEX_PREVIOUS_RESPONSE_STALE_SUBFIELD_GUARD}))` +
|
|
157
|
+
`|${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
|
+
"i",
|
|
159
|
+
);
|
|
118
160
|
const CODEX_RETRYABLE_EVENT_CODES = new Set(["model_error", "server_error", "internal_error"]);
|
|
119
161
|
const CODEX_NON_RETRYABLE_EVENT_CODES = new Set([
|
|
120
162
|
"invalid_function_parameters",
|
|
@@ -163,10 +205,21 @@ const CODEX_PROGRESS_EVENT_TYPES = new Set([
|
|
|
163
205
|
"error",
|
|
164
206
|
]);
|
|
165
207
|
|
|
208
|
+
/**
|
|
209
|
+
* A progress event must carry real semantic payload, matching the Anthropic
|
|
210
|
+
* predicate: a recognized envelope whose `delta` is absent or not a non-empty
|
|
211
|
+
* string is NOT progress — otherwise repeated malformed/no-op deltas reset the
|
|
212
|
+
* idle watchdog indefinitely and a managed attempt need never terminate.
|
|
213
|
+
* Non-delta envelope types (lifecycle, item boundaries, terminal events) count
|
|
214
|
+
* as progress by type alone, as before.
|
|
215
|
+
*/
|
|
166
216
|
function isCodexStreamProgressEvent(event: unknown): boolean {
|
|
167
217
|
if (!event || typeof event !== "object") return false;
|
|
168
218
|
const type = (event as { type?: unknown }).type;
|
|
169
|
-
|
|
219
|
+
if (typeof type !== "string" || !CODEX_PROGRESS_EVENT_TYPES.has(type)) return false;
|
|
220
|
+
if (!type.endsWith(".delta")) return true;
|
|
221
|
+
const delta = (event as { delta?: unknown }).delta;
|
|
222
|
+
return typeof delta === "string" && delta.length > 0;
|
|
170
223
|
}
|
|
171
224
|
type CodexTransport = "sse" | "websocket";
|
|
172
225
|
interface CodexInitialTransport {
|
|
@@ -174,10 +227,12 @@ interface CodexInitialTransport {
|
|
|
174
227
|
requestBodyForState: RequestBody;
|
|
175
228
|
transport: CodexTransport;
|
|
176
229
|
toolChoiceFallbackApplied?: boolean;
|
|
230
|
+
/** Whether the dispatched request actually carried a `previous_response_id` anchor. */
|
|
231
|
+
sentPreviousResponseId?: boolean;
|
|
177
232
|
}
|
|
178
233
|
type CodexEventItem = ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall | ResponseCustomToolCall;
|
|
179
234
|
type CodexThinkingBlock = ThinkingContent & { summaryBuffer: string; rawBuffer: string; summaryStarted: boolean };
|
|
180
|
-
type CodexOutputBlock = CodexThinkingBlock | TextContent | (ToolCall & { partialJson: string });
|
|
235
|
+
type CodexOutputBlock = CodexThinkingBlock | TextContent | (ToolCall & { partialJson: string; doneInput?: string });
|
|
181
236
|
export interface OpenAICodexWebSocketDebugStats {
|
|
182
237
|
fullContextRequests: number;
|
|
183
238
|
deltaRequests: number;
|
|
@@ -277,11 +332,27 @@ interface CodexStreamRuntime {
|
|
|
277
332
|
websocketStreamRetries: number;
|
|
278
333
|
providerRetryAttempt: number;
|
|
279
334
|
toolChoiceFallbackAttempted: boolean;
|
|
335
|
+
/**
|
|
336
|
+
* Stale-anchor recovery is one-shot. Once the anchor is cleared the replay no
|
|
337
|
+
* longer carries `previous_response_id`, so a repeated rejection is a real
|
|
338
|
+
* fault and must surface instead of replaying full context up to five times.
|
|
339
|
+
*/
|
|
340
|
+
previousResponseRecoveryAttempted: boolean;
|
|
341
|
+
/**
|
|
342
|
+
* Whether the in-flight request actually carried a `previous_response_id`.
|
|
343
|
+
* Anchor recovery is only meaningful when it did: a rejection naming the field
|
|
344
|
+
* on an anchor-free request (`previous_response_id is required`) is a real
|
|
345
|
+
* validation fault, and clearing session state plus resending the identical
|
|
346
|
+
* body would destroy valid metadata to no effect.
|
|
347
|
+
*/
|
|
348
|
+
sentPreviousResponseId: boolean;
|
|
280
349
|
sseRequestBodyOverride?: RequestBody;
|
|
281
350
|
sawTerminalEvent: boolean;
|
|
282
351
|
canSafelyReplayWebsocketOverSse: boolean;
|
|
283
352
|
/** Ids of tool calls that received their terminal `output_item.done`. */
|
|
284
353
|
finalizedToolCallIds: Set<string>;
|
|
354
|
+
/** Event types whose degraded non-string increment was already diagnosed. */
|
|
355
|
+
degradedIncrementDiagnostics: Set<string>;
|
|
285
356
|
}
|
|
286
357
|
|
|
287
358
|
interface CodexStreamProcessingContext {
|
|
@@ -520,8 +591,13 @@ function createEmptyUsage(): AssistantMessage["usage"] {
|
|
|
520
591
|
};
|
|
521
592
|
}
|
|
522
593
|
|
|
594
|
+
/** @internal Exported for tests. */
|
|
595
|
+
export function formatCodexUserAgent(platform: string, release: string, arch: string): string {
|
|
596
|
+
return `pi/${packageJson.version} (${sanitizeHeaderComponent(platform)} ${sanitizeHeaderComponent(release)}; ${sanitizeHeaderComponent(arch)})`;
|
|
597
|
+
}
|
|
598
|
+
|
|
523
599
|
function getCodexUserAgent(): string {
|
|
524
|
-
return
|
|
600
|
+
return formatCodexUserAgent(os.platform(), os.release(), os.arch());
|
|
525
601
|
}
|
|
526
602
|
|
|
527
603
|
function getCodexServiceTierCostMultiplier(
|
|
@@ -592,6 +668,10 @@ function resetOutputState(output: AssistantMessage): void {
|
|
|
592
668
|
function removeTransientBlockIndices(output: AssistantMessage): void {
|
|
593
669
|
for (const block of output.content) {
|
|
594
670
|
delete (block as { index?: number }).index;
|
|
671
|
+
if (block.type === "toolCall") {
|
|
672
|
+
delete (block as { partialJson?: string }).partialJson;
|
|
673
|
+
delete (block as { doneInput?: string }).doneInput;
|
|
674
|
+
}
|
|
595
675
|
}
|
|
596
676
|
}
|
|
597
677
|
|
|
@@ -808,8 +888,10 @@ async function openCodexWebSocketTransport(
|
|
|
808
888
|
eventStream: AsyncGenerator<Record<string, unknown>>;
|
|
809
889
|
requestBodyForState: RequestBody;
|
|
810
890
|
transport: CodexTransport;
|
|
891
|
+
sentPreviousResponseId: boolean;
|
|
811
892
|
}> {
|
|
812
893
|
const websocketRequest = buildCodexWebSocketRequest(requestContext.transformedBody, websocketState);
|
|
894
|
+
const sentPreviousResponseId = typeof websocketRequest.previous_response_id === "string";
|
|
813
895
|
const websocketHeaders = createCodexHeaders(
|
|
814
896
|
requestContext.requestHeaders,
|
|
815
897
|
requestContext.accountId,
|
|
@@ -827,6 +909,7 @@ async function openCodexWebSocketTransport(
|
|
|
827
909
|
sentTurnStateHeader: websocketHeaders.has(X_CODEX_TURN_STATE_HEADER),
|
|
828
910
|
sentModelsEtagHeader: websocketHeaders.has(X_MODELS_ETAG_HEADER),
|
|
829
911
|
requestType: websocketRequest.type,
|
|
912
|
+
sentPreviousResponseId,
|
|
830
913
|
retry,
|
|
831
914
|
retryBudget: getCodexWebSocketRetryBudget(options),
|
|
832
915
|
});
|
|
@@ -839,7 +922,7 @@ async function openCodexWebSocketTransport(
|
|
|
839
922
|
options,
|
|
840
923
|
requestSetup.firstEventTimeoutMs,
|
|
841
924
|
);
|
|
842
|
-
return { eventStream, requestBodyForState, transport: "websocket" };
|
|
925
|
+
return { eventStream, requestBodyForState, transport: "websocket", sentPreviousResponseId };
|
|
843
926
|
}
|
|
844
927
|
|
|
845
928
|
async function openCodexSseTransport(
|
|
@@ -904,6 +987,7 @@ async function reopenCodexWebSocketRuntimeStream(
|
|
|
904
987
|
runtime.eventStream = next.eventStream;
|
|
905
988
|
runtime.requestBodyForState = next.requestBodyForState;
|
|
906
989
|
runtime.transport = next.transport;
|
|
990
|
+
runtime.sentPreviousResponseId = next.sentPreviousResponseId === true;
|
|
907
991
|
state.lastTransport = next.transport;
|
|
908
992
|
} catch (error) {
|
|
909
993
|
const wsError = error instanceof Error ? error : new Error(String(error));
|
|
@@ -936,6 +1020,8 @@ async function reopenCodexSseRuntimeStream(
|
|
|
936
1020
|
runtime.eventStream = next.eventStream;
|
|
937
1021
|
runtime.requestBodyForState = next.requestBodyForState;
|
|
938
1022
|
runtime.transport = next.transport;
|
|
1023
|
+
// SSE never attaches `previous_response_id`; only buildCodexWebSocketRequest does.
|
|
1024
|
+
runtime.sentPreviousResponseId = false;
|
|
939
1025
|
if (state) {
|
|
940
1026
|
state.lastTransport = next.transport;
|
|
941
1027
|
}
|
|
@@ -947,6 +1033,7 @@ function createCodexStreamRuntime(initial: {
|
|
|
947
1033
|
transport: CodexTransport;
|
|
948
1034
|
websocketState?: CodexWebSocketSessionState;
|
|
949
1035
|
toolChoiceFallbackApplied?: boolean;
|
|
1036
|
+
sentPreviousResponseId?: boolean;
|
|
950
1037
|
}): CodexStreamRuntime {
|
|
951
1038
|
return {
|
|
952
1039
|
eventStream: initial.eventStream,
|
|
@@ -959,12 +1046,15 @@ function createCodexStreamRuntime(initial: {
|
|
|
959
1046
|
websocketStreamRetries: 0,
|
|
960
1047
|
providerRetryAttempt: 0,
|
|
961
1048
|
toolChoiceFallbackAttempted: initial.toolChoiceFallbackApplied === true,
|
|
1049
|
+
previousResponseRecoveryAttempted: false,
|
|
1050
|
+
sentPreviousResponseId: initial.sentPreviousResponseId === true,
|
|
962
1051
|
sseRequestBodyOverride: initial.toolChoiceFallbackApplied
|
|
963
1052
|
? structuredCloneJSON(initial.requestBodyForState)
|
|
964
1053
|
: undefined,
|
|
965
1054
|
sawTerminalEvent: false,
|
|
966
1055
|
canSafelyReplayWebsocketOverSse: true,
|
|
967
1056
|
finalizedToolCallIds: new Set<string>(),
|
|
1057
|
+
degradedIncrementDiagnostics: new Set<string>(),
|
|
968
1058
|
};
|
|
969
1059
|
}
|
|
970
1060
|
|
|
@@ -1019,6 +1109,13 @@ function handleCodexStreamEvent(args: {
|
|
|
1019
1109
|
runtime.currentItem = item;
|
|
1020
1110
|
runtime.currentBlock = createOutputBlockForItem(item);
|
|
1021
1111
|
if (!runtime.currentBlock) return firstTokenTime;
|
|
1112
|
+
const currentBlock = runtime.currentBlock;
|
|
1113
|
+
if (
|
|
1114
|
+
currentBlock.type === "toolCall" &&
|
|
1115
|
+
output.content.some(block => block.type === "toolCall" && block.id === currentBlock.id)
|
|
1116
|
+
) {
|
|
1117
|
+
throw new Error("Codex stream reused an active tool-call identifier");
|
|
1118
|
+
}
|
|
1022
1119
|
output.content.push(runtime.currentBlock);
|
|
1023
1120
|
stream.push({
|
|
1024
1121
|
type: getOutputBlockStartEventType(runtime.currentBlock),
|
|
@@ -1034,7 +1131,13 @@ function handleCodexStreamEvent(args: {
|
|
|
1034
1131
|
}
|
|
1035
1132
|
|
|
1036
1133
|
if (eventType === "response.reasoning_summary_text.delta") {
|
|
1037
|
-
|
|
1134
|
+
const delta = normalizeCodexIncrement(
|
|
1135
|
+
rawEvent,
|
|
1136
|
+
"response.reasoning_summary_text.delta",
|
|
1137
|
+
model,
|
|
1138
|
+
runtime.degradedIncrementDiagnostics,
|
|
1139
|
+
);
|
|
1140
|
+
handleReasoningSummaryTextDelta(runtime.currentItem, runtime.currentBlock, delta, stream, output, blockIndex);
|
|
1038
1141
|
return firstTokenTime;
|
|
1039
1142
|
}
|
|
1040
1143
|
|
|
@@ -1044,7 +1147,13 @@ function handleCodexStreamEvent(args: {
|
|
|
1044
1147
|
}
|
|
1045
1148
|
|
|
1046
1149
|
if (eventType === "response.reasoning_text.delta") {
|
|
1047
|
-
|
|
1150
|
+
const delta = normalizeCodexIncrement(
|
|
1151
|
+
rawEvent,
|
|
1152
|
+
"response.reasoning_text.delta",
|
|
1153
|
+
model,
|
|
1154
|
+
runtime.degradedIncrementDiagnostics,
|
|
1155
|
+
);
|
|
1156
|
+
handleReasoningTextDelta(runtime.currentItem, runtime.currentBlock, delta, stream, output, blockIndex);
|
|
1048
1157
|
return firstTokenTime;
|
|
1049
1158
|
}
|
|
1050
1159
|
|
|
@@ -1054,10 +1163,16 @@ function handleCodexStreamEvent(args: {
|
|
|
1054
1163
|
}
|
|
1055
1164
|
|
|
1056
1165
|
if (eventType === "response.output_text.delta") {
|
|
1166
|
+
const delta = normalizeCodexIncrement(
|
|
1167
|
+
rawEvent,
|
|
1168
|
+
"response.output_text.delta",
|
|
1169
|
+
model,
|
|
1170
|
+
runtime.degradedIncrementDiagnostics,
|
|
1171
|
+
);
|
|
1057
1172
|
handleMessageTextDelta(
|
|
1058
1173
|
runtime.currentItem,
|
|
1059
1174
|
runtime.currentBlock,
|
|
1060
|
-
|
|
1175
|
+
delta,
|
|
1061
1176
|
stream,
|
|
1062
1177
|
output,
|
|
1063
1178
|
blockIndex,
|
|
@@ -1067,20 +1182,19 @@ function handleCodexStreamEvent(args: {
|
|
|
1067
1182
|
}
|
|
1068
1183
|
|
|
1069
1184
|
if (eventType === "response.refusal.delta") {
|
|
1070
|
-
|
|
1071
|
-
runtime.currentItem,
|
|
1072
|
-
runtime.currentBlock,
|
|
1185
|
+
const delta = normalizeCodexIncrement(
|
|
1073
1186
|
rawEvent,
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
"refusal",
|
|
1187
|
+
"response.refusal.delta",
|
|
1188
|
+
model,
|
|
1189
|
+
runtime.degradedIncrementDiagnostics,
|
|
1078
1190
|
);
|
|
1191
|
+
handleMessageTextDelta(runtime.currentItem, runtime.currentBlock, delta, stream, output, blockIndex, "refusal");
|
|
1079
1192
|
return firstTokenTime;
|
|
1080
1193
|
}
|
|
1081
1194
|
|
|
1082
1195
|
if (eventType === "response.function_call_arguments.delta") {
|
|
1083
|
-
|
|
1196
|
+
const delta = assertStringToolArgumentIncrement(rawEvent, "response.function_call_arguments.delta");
|
|
1197
|
+
handleToolCallArgumentsDelta(runtime.currentItem, runtime.currentBlock, delta, stream, output, blockIndex);
|
|
1084
1198
|
return firstTokenTime;
|
|
1085
1199
|
}
|
|
1086
1200
|
|
|
@@ -1090,7 +1204,8 @@ function handleCodexStreamEvent(args: {
|
|
|
1090
1204
|
}
|
|
1091
1205
|
|
|
1092
1206
|
if (eventType === "response.custom_tool_call_input.delta") {
|
|
1093
|
-
|
|
1207
|
+
const delta = assertStringToolArgumentIncrement(rawEvent, "response.custom_tool_call_input.delta");
|
|
1208
|
+
handleCustomToolCallInputDelta(runtime.currentItem, runtime.currentBlock, delta, stream, output, blockIndex);
|
|
1094
1209
|
return firstTokenTime;
|
|
1095
1210
|
}
|
|
1096
1211
|
|
|
@@ -1137,6 +1252,10 @@ function createOutputBlockForItem(item: CodexEventItem): CodexOutputBlock | null
|
|
|
1137
1252
|
};
|
|
1138
1253
|
}
|
|
1139
1254
|
if (item.type === "custom_tool_call") {
|
|
1255
|
+
const initialInput: unknown = item.input;
|
|
1256
|
+
if (typeof initialInput !== "string") {
|
|
1257
|
+
throw new Error("Codex custom_tool_call started with non-string input");
|
|
1258
|
+
}
|
|
1140
1259
|
// Wire name flows through unchanged; the agent-loop dispatcher also
|
|
1141
1260
|
// matches `Tool.customWireName`. Reuse `partialJson` as the
|
|
1142
1261
|
// accumulation buffer for the raw input string.
|
|
@@ -1144,9 +1263,9 @@ function createOutputBlockForItem(item: CodexEventItem): CodexOutputBlock | null
|
|
|
1144
1263
|
type: "toolCall",
|
|
1145
1264
|
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
1146
1265
|
name: item.name,
|
|
1147
|
-
arguments: { input:
|
|
1266
|
+
arguments: { input: initialInput },
|
|
1148
1267
|
customWireName: item.name,
|
|
1149
|
-
partialJson:
|
|
1268
|
+
partialJson: initialInput,
|
|
1150
1269
|
};
|
|
1151
1270
|
}
|
|
1152
1271
|
return null;
|
|
@@ -1164,10 +1283,52 @@ function handleReasoningSummaryPartAdded(currentItem: CodexEventItem | null, raw
|
|
|
1164
1283
|
currentItem.summary.push((rawEvent as { part: ResponseReasoningItem["summary"][number] }).part);
|
|
1165
1284
|
}
|
|
1166
1285
|
|
|
1286
|
+
/**
|
|
1287
|
+
* Primitive anomalies (undefined, null, numbers, booleans) stay coerced to
|
|
1288
|
+
* an empty string by the increment handlers. Diagnose at most once per event
|
|
1289
|
+
* type per stream, naming only the envelope shape — never the payload.
|
|
1290
|
+
*/
|
|
1291
|
+
function normalizeCodexIncrement(
|
|
1292
|
+
rawEvent: Record<string, unknown>,
|
|
1293
|
+
eventType: string,
|
|
1294
|
+
model: Model<"openai-codex-responses">,
|
|
1295
|
+
degradedIncrementDiagnostics: Set<string>,
|
|
1296
|
+
): string {
|
|
1297
|
+
const raw = (rawEvent as { delta?: unknown }).delta;
|
|
1298
|
+
if (typeof raw === "string") return raw;
|
|
1299
|
+
if (!degradedIncrementDiagnostics.has(eventType)) {
|
|
1300
|
+
degradedIncrementDiagnostics.add(eventType);
|
|
1301
|
+
logger.warn("codex: degraded non-string stream increment to empty string", {
|
|
1302
|
+
model: model.id,
|
|
1303
|
+
provider: model.provider,
|
|
1304
|
+
eventType,
|
|
1305
|
+
receivedType: raw === null ? "null" : typeof raw,
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
return "";
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* Tool-argument fragments are positional JSON text. Erasing or coercing ANY
|
|
1313
|
+
* malformed increment — primitive, object, or function — assembles
|
|
1314
|
+
* valid-but-wrong arguments (e.g. `{"n":1` + numeric primitive erased to ""
|
|
1315
|
+
* + `3}` parses as {"n":13} and executes), so the turn fails closed on every
|
|
1316
|
+
* non-string delta. The payload never enters the error message.
|
|
1317
|
+
*/
|
|
1318
|
+
function assertStringToolArgumentIncrement(rawEvent: Record<string, unknown>, eventType: string): string {
|
|
1319
|
+
const raw = (rawEvent as { delta?: unknown }).delta;
|
|
1320
|
+
if (typeof raw !== "string") {
|
|
1321
|
+
throw new Error(
|
|
1322
|
+
`Codex stream sent a non-string ${eventType} tool-argument increment; failing the turn instead of assembling wrong tool arguments`,
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
return raw;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1167
1328
|
function handleReasoningSummaryTextDelta(
|
|
1168
1329
|
currentItem: CodexEventItem | null,
|
|
1169
1330
|
currentBlock: CodexOutputBlock | null,
|
|
1170
|
-
|
|
1331
|
+
delta: string,
|
|
1171
1332
|
stream: AssistantMessageEventStream,
|
|
1172
1333
|
output: AssistantMessage,
|
|
1173
1334
|
blockIndex: () => number,
|
|
@@ -1180,7 +1341,6 @@ function handleReasoningSummaryTextDelta(
|
|
|
1180
1341
|
currentItem.summary = currentItem.summary || [];
|
|
1181
1342
|
const lastPart = currentItem.summary[currentItem.summary.length - 1];
|
|
1182
1343
|
if (!lastPart) return;
|
|
1183
|
-
const delta = (rawEvent as { delta?: string }).delta || "";
|
|
1184
1344
|
currentBlock.thinking += delta;
|
|
1185
1345
|
currentBlock.summaryBuffer += delta;
|
|
1186
1346
|
lastPart.text += delta;
|
|
@@ -1207,13 +1367,12 @@ function handleReasoningSummaryPartDone(
|
|
|
1207
1367
|
function handleReasoningTextDelta(
|
|
1208
1368
|
currentItem: CodexEventItem | null,
|
|
1209
1369
|
currentBlock: CodexOutputBlock | null,
|
|
1210
|
-
|
|
1370
|
+
delta: string,
|
|
1211
1371
|
stream: AssistantMessageEventStream,
|
|
1212
1372
|
output: AssistantMessage,
|
|
1213
1373
|
blockIndex: () => number,
|
|
1214
1374
|
): void {
|
|
1215
1375
|
if (currentItem?.type !== "reasoning" || currentBlock?.type !== "thinking") return;
|
|
1216
|
-
const delta = (rawEvent as { delta?: string }).delta || "";
|
|
1217
1376
|
currentBlock.thinking += delta;
|
|
1218
1377
|
currentBlock.rawBuffer += delta;
|
|
1219
1378
|
stream.push({ type: "thinking_delta", contentIndex: blockIndex(), delta, partial: output });
|
|
@@ -1231,7 +1390,7 @@ function handleContentPartAdded(currentItem: CodexEventItem | null, rawEvent: Re
|
|
|
1231
1390
|
function handleMessageTextDelta(
|
|
1232
1391
|
currentItem: CodexEventItem | null,
|
|
1233
1392
|
currentBlock: CodexOutputBlock | null,
|
|
1234
|
-
|
|
1393
|
+
delta: string,
|
|
1235
1394
|
stream: AssistantMessageEventStream,
|
|
1236
1395
|
output: AssistantMessage,
|
|
1237
1396
|
blockIndex: () => number,
|
|
@@ -1241,7 +1400,6 @@ function handleMessageTextDelta(
|
|
|
1241
1400
|
if (!currentItem.content || currentItem.content.length === 0) return;
|
|
1242
1401
|
const lastPart = currentItem.content[currentItem.content.length - 1];
|
|
1243
1402
|
if (!lastPart || lastPart.type !== partType) return;
|
|
1244
|
-
const delta = (rawEvent as { delta?: string }).delta || "";
|
|
1245
1403
|
currentBlock.text += delta;
|
|
1246
1404
|
if (lastPart.type === "output_text") {
|
|
1247
1405
|
lastPart.text += delta;
|
|
@@ -1254,13 +1412,12 @@ function handleMessageTextDelta(
|
|
|
1254
1412
|
function handleToolCallArgumentsDelta(
|
|
1255
1413
|
currentItem: CodexEventItem | null,
|
|
1256
1414
|
currentBlock: CodexOutputBlock | null,
|
|
1257
|
-
|
|
1415
|
+
delta: string,
|
|
1258
1416
|
stream: AssistantMessageEventStream,
|
|
1259
1417
|
output: AssistantMessage,
|
|
1260
1418
|
blockIndex: () => number,
|
|
1261
1419
|
): void {
|
|
1262
1420
|
if (currentItem?.type !== "function_call" || currentBlock?.type !== "toolCall") return;
|
|
1263
|
-
const delta = (rawEvent as { delta?: string }).delta || "";
|
|
1264
1421
|
currentBlock.partialJson += delta;
|
|
1265
1422
|
currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
|
|
1266
1423
|
stream.push({ type: "toolcall_delta", contentIndex: blockIndex(), delta, partial: output });
|
|
@@ -1283,13 +1440,12 @@ function handleToolCallArgumentsDone(
|
|
|
1283
1440
|
function handleCustomToolCallInputDelta(
|
|
1284
1441
|
currentItem: CodexEventItem | null,
|
|
1285
1442
|
currentBlock: CodexOutputBlock | null,
|
|
1286
|
-
|
|
1443
|
+
delta: string,
|
|
1287
1444
|
stream: AssistantMessageEventStream,
|
|
1288
1445
|
output: AssistantMessage,
|
|
1289
1446
|
blockIndex: () => number,
|
|
1290
1447
|
): void {
|
|
1291
1448
|
if (currentItem?.type !== "custom_tool_call" || currentBlock?.type !== "toolCall") return;
|
|
1292
|
-
const delta = (rawEvent as { delta?: string }).delta || "";
|
|
1293
1449
|
currentBlock.partialJson += delta;
|
|
1294
1450
|
currentBlock.arguments = { input: currentBlock.partialJson };
|
|
1295
1451
|
stream.push({ type: "toolcall_delta", contentIndex: blockIndex(), delta, partial: output });
|
|
@@ -1301,11 +1457,17 @@ function handleCustomToolCallInputDone(
|
|
|
1301
1457
|
rawEvent: Record<string, unknown>,
|
|
1302
1458
|
): void {
|
|
1303
1459
|
if (currentItem?.type !== "custom_tool_call" || currentBlock?.type !== "toolCall") return;
|
|
1304
|
-
const input = (rawEvent as { input?:
|
|
1305
|
-
if (typeof input
|
|
1306
|
-
|
|
1307
|
-
currentBlock.arguments = { input };
|
|
1460
|
+
const input = (rawEvent as { input?: unknown }).input;
|
|
1461
|
+
if (typeof input !== "string") {
|
|
1462
|
+
throw new Error("Codex stream sent non-string input in custom_tool_call_input.done");
|
|
1308
1463
|
}
|
|
1464
|
+
if (currentBlock.partialJson && currentBlock.partialJson !== input) {
|
|
1465
|
+
throw new Error(
|
|
1466
|
+
"Codex custom_tool_call input.done disagrees with the streamed input buffer; failing the turn instead of executing corrupted input",
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
currentBlock.doneInput = input;
|
|
1470
|
+
currentBlock.arguments = { input };
|
|
1309
1471
|
}
|
|
1310
1472
|
|
|
1311
1473
|
function handleOutputItemDone(
|
|
@@ -1385,36 +1547,90 @@ function handleOutputItemDone(
|
|
|
1385
1547
|
}
|
|
1386
1548
|
|
|
1387
1549
|
if (item.type === "function_call") {
|
|
1550
|
+
if (typeof item.arguments !== "string") {
|
|
1551
|
+
throw new Error("Codex function_call completed with non-string terminal arguments");
|
|
1552
|
+
}
|
|
1553
|
+
let terminalArguments: unknown;
|
|
1554
|
+
try {
|
|
1555
|
+
terminalArguments = JSON.parse(item.arguments);
|
|
1556
|
+
} catch {
|
|
1557
|
+
throw new Error("Codex function_call completed with malformed terminal arguments");
|
|
1558
|
+
}
|
|
1559
|
+
if (!terminalArguments || typeof terminalArguments !== "object" || Array.isArray(terminalArguments)) {
|
|
1560
|
+
throw new Error("Codex function_call terminal arguments were not a JSON object");
|
|
1561
|
+
}
|
|
1388
1562
|
const id = encodeResponsesToolCallId(item.call_id, item.id);
|
|
1563
|
+
if (runtime.currentBlock?.type !== "toolCall" || runtime.currentBlock.id !== id) {
|
|
1564
|
+
throw new Error("Codex function_call terminal item did not match the active tool call");
|
|
1565
|
+
}
|
|
1389
1566
|
runtime.finalizedToolCallIds.add(id);
|
|
1390
1567
|
const toolCall: ToolCall = {
|
|
1391
1568
|
type: "toolCall",
|
|
1392
1569
|
id,
|
|
1393
1570
|
name: codexToolCanonicalName(item.name),
|
|
1394
|
-
arguments:
|
|
1571
|
+
arguments: terminalArguments as Record<string, unknown>,
|
|
1395
1572
|
...(findUnnecessaryUnicodeEscape(item.arguments || "") ? { escapedNonAsciiArguments: true } : {}),
|
|
1396
1573
|
};
|
|
1574
|
+
Object.assign(runtime.currentBlock, toolCall);
|
|
1575
|
+
delete (runtime.currentBlock as { partialJson?: string }).partialJson;
|
|
1576
|
+
delete (runtime.currentBlock as { doneInput?: string }).doneInput;
|
|
1397
1577
|
runtime.canSafelyReplayWebsocketOverSse = false;
|
|
1398
1578
|
stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
|
|
1579
|
+
runtime.currentItem = null;
|
|
1580
|
+
runtime.currentBlock = null;
|
|
1399
1581
|
return;
|
|
1400
1582
|
}
|
|
1401
1583
|
|
|
1402
1584
|
if (item.type === "custom_tool_call") {
|
|
1585
|
+
const terminalInput: unknown = item.input;
|
|
1586
|
+
if (typeof terminalInput !== "string") {
|
|
1587
|
+
throw new Error(
|
|
1588
|
+
"Codex custom_tool_call completed with non-string terminal input; failing the turn instead of finalizing from the streamed buffer",
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1403
1591
|
const id = encodeResponsesToolCallId(item.call_id, item.id);
|
|
1592
|
+
if (runtime.currentBlock?.type !== "toolCall" || runtime.currentBlock.id !== id) {
|
|
1593
|
+
throw new Error("Codex custom_tool_call terminal item did not match the active tool call");
|
|
1594
|
+
}
|
|
1404
1595
|
runtime.finalizedToolCallIds.add(id);
|
|
1405
|
-
|
|
1596
|
+
// The terminal `output_item.done.item.input` is the authoritative
|
|
1597
|
+
// complete input; the streamed `partialJson` buffer is advisory. If both
|
|
1598
|
+
// exist and disagree, the stream was corrupted (dropped/malformed
|
|
1599
|
+
// increments), so fail closed instead of executing either variant —
|
|
1600
|
+
// matching function-call finalization, which always trusts the terminal
|
|
1601
|
+
// `item.arguments`.
|
|
1602
|
+
const streamedInput =
|
|
1406
1603
|
runtime.currentBlock?.type === "toolCall" && runtime.currentBlock.partialJson
|
|
1407
1604
|
? runtime.currentBlock.partialJson
|
|
1408
|
-
:
|
|
1605
|
+
: undefined;
|
|
1606
|
+
if (streamedInput !== undefined && streamedInput !== terminalInput) {
|
|
1607
|
+
throw new Error(
|
|
1608
|
+
"Codex custom_tool_call terminal input disagrees with the streamed input buffer; failing the turn instead of executing a corrupted tool call",
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
if (
|
|
1612
|
+
runtime.currentBlock?.type === "toolCall" &&
|
|
1613
|
+
runtime.currentBlock.doneInput !== undefined &&
|
|
1614
|
+
runtime.currentBlock.doneInput !== terminalInput
|
|
1615
|
+
) {
|
|
1616
|
+
throw new Error(
|
|
1617
|
+
"Codex custom_tool_call terminal input disagrees with input.done; failing the turn instead of executing conflicting input",
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1409
1620
|
const toolCall: ToolCall = {
|
|
1410
1621
|
type: "toolCall",
|
|
1411
1622
|
id,
|
|
1412
1623
|
name: item.name,
|
|
1413
|
-
arguments: { input:
|
|
1624
|
+
arguments: { input: terminalInput },
|
|
1414
1625
|
customWireName: item.name,
|
|
1415
1626
|
};
|
|
1627
|
+
Object.assign(runtime.currentBlock, toolCall);
|
|
1628
|
+
delete (runtime.currentBlock as { partialJson?: string }).partialJson;
|
|
1629
|
+
delete (runtime.currentBlock as { doneInput?: string }).doneInput;
|
|
1416
1630
|
runtime.canSafelyReplayWebsocketOverSse = false;
|
|
1417
1631
|
stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
|
|
1632
|
+
runtime.currentItem = null;
|
|
1633
|
+
runtime.currentBlock = null;
|
|
1418
1634
|
return;
|
|
1419
1635
|
}
|
|
1420
1636
|
|
|
@@ -1476,6 +1692,12 @@ function handleResponseCompleted(
|
|
|
1476
1692
|
// call that never received its `output_item.done` so the agent loop rejects
|
|
1477
1693
|
// the truncated arguments instead of executing a best-effort partial parse.
|
|
1478
1694
|
flagTruncatedToolCalls(output, output.stopReason, block => runtime.finalizedToolCallIds.has(block.id));
|
|
1695
|
+
if (
|
|
1696
|
+
output.stopReason === "stop" &&
|
|
1697
|
+
output.content.some(block => block.type === "toolCall" && !runtime.finalizedToolCallIds.has(block.id))
|
|
1698
|
+
) {
|
|
1699
|
+
throw new Error("Codex response completed with an unfinalized tool call");
|
|
1700
|
+
}
|
|
1479
1701
|
if (output.content.some(block => block.type === "toolCall") && output.stopReason === "stop") {
|
|
1480
1702
|
output.stopReason = "toolUse";
|
|
1481
1703
|
}
|
|
@@ -1540,6 +1762,7 @@ async function tryRetryWithoutForcedToolChoice(
|
|
|
1540
1762
|
runtime.currentBlock = null;
|
|
1541
1763
|
runtime.sawTerminalEvent = false;
|
|
1542
1764
|
runtime.nativeOutputItems.length = 0;
|
|
1765
|
+
runtime.finalizedToolCallIds.clear();
|
|
1543
1766
|
resetOutputState(context.output);
|
|
1544
1767
|
context.firstTokenTime = undefined;
|
|
1545
1768
|
|
|
@@ -1559,6 +1782,7 @@ async function tryRetryWithoutForcedToolChoice(
|
|
|
1559
1782
|
runtime.requestBodyForState = next.requestBodyForState;
|
|
1560
1783
|
runtime.sseRequestBodyOverride = next.requestBodyForState;
|
|
1561
1784
|
runtime.transport = next.transport;
|
|
1785
|
+
runtime.sentPreviousResponseId = false;
|
|
1562
1786
|
if (websocketState) {
|
|
1563
1787
|
websocketState.lastTransport = next.transport;
|
|
1564
1788
|
}
|
|
@@ -1635,6 +1859,7 @@ async function tryReconnectCodexWebSocketOnConnectionLimit(
|
|
|
1635
1859
|
runtime.currentItem = null;
|
|
1636
1860
|
runtime.currentBlock = null;
|
|
1637
1861
|
runtime.nativeOutputItems.length = 0;
|
|
1862
|
+
runtime.finalizedToolCallIds.clear();
|
|
1638
1863
|
resetOutputState(context.output);
|
|
1639
1864
|
context.firstTokenTime = undefined;
|
|
1640
1865
|
recordCodexWebSocketFailure(websocketState, true);
|
|
@@ -1649,10 +1874,13 @@ async function tryReconnectCodexWebSocketOnConnectionLimit(
|
|
|
1649
1874
|
}
|
|
1650
1875
|
|
|
1651
1876
|
function isCodexPreviousResponseNotFound(error: unknown): boolean {
|
|
1877
|
+
if (!(error instanceof CodexProviderStreamError)) return false;
|
|
1878
|
+
if (typeof error.code === "string" && CODEX_PREVIOUS_RESPONSE_STALE_CODES.has(error.code)) return true;
|
|
1879
|
+
// Raw provider message only — `error.message` carries appended `code=` metadata
|
|
1880
|
+
// that would supply the stale qualifier the provider never sent.
|
|
1652
1881
|
return (
|
|
1653
|
-
error
|
|
1654
|
-
|
|
1655
|
-
CODEX_PREVIOUS_RESPONSE_STALE_CODES.has(error.code)
|
|
1882
|
+
CODEX_PREVIOUS_RESPONSE_STALE_MESSAGE.test(error.providerMessage) ||
|
|
1883
|
+
CODEX_PREVIOUS_RESPONSE_STALE_PROSE_MESSAGE.test(error.providerMessage)
|
|
1656
1884
|
);
|
|
1657
1885
|
}
|
|
1658
1886
|
|
|
@@ -1664,6 +1892,12 @@ async function tryRecoverCodexPreviousResponseNotFound(
|
|
|
1664
1892
|
const websocketState = context.requestContext.websocketState;
|
|
1665
1893
|
if (
|
|
1666
1894
|
!isCodexPreviousResponseNotFound(error) ||
|
|
1895
|
+
// An anchor rejection is only actionable when this request actually sent one.
|
|
1896
|
+
// `previous_response_id is required` on an anchor-free request is a genuine
|
|
1897
|
+
// validation fault: clearing session metadata and resending the identical
|
|
1898
|
+
// body cannot fix it.
|
|
1899
|
+
!runtime.sentPreviousResponseId ||
|
|
1900
|
+
runtime.previousResponseRecoveryAttempted ||
|
|
1667
1901
|
!websocketState ||
|
|
1668
1902
|
context.options?.fallbackManaged ||
|
|
1669
1903
|
runtime.transport !== "websocket" ||
|
|
@@ -1675,12 +1909,14 @@ async function tryRecoverCodexPreviousResponseNotFound(
|
|
|
1675
1909
|
}
|
|
1676
1910
|
|
|
1677
1911
|
runtime.providerRetryAttempt += 1;
|
|
1912
|
+
runtime.previousResponseRecoveryAttempted = true;
|
|
1678
1913
|
resetCodexWebSocketAppendState(websocketState);
|
|
1679
1914
|
resetCodexSessionMetadata(websocketState);
|
|
1680
1915
|
runtime.currentItem = null;
|
|
1681
1916
|
runtime.currentBlock = null;
|
|
1682
1917
|
runtime.sawTerminalEvent = false;
|
|
1683
1918
|
runtime.nativeOutputItems.length = 0;
|
|
1919
|
+
runtime.finalizedToolCallIds.clear();
|
|
1684
1920
|
resetOutputState(context.output);
|
|
1685
1921
|
context.firstTokenTime = undefined;
|
|
1686
1922
|
|
|
@@ -1739,6 +1975,7 @@ async function tryReplayWebsocketFailureOverSse(
|
|
|
1739
1975
|
runtime.currentItem = null;
|
|
1740
1976
|
runtime.currentBlock = null;
|
|
1741
1977
|
runtime.nativeOutputItems.length = 0;
|
|
1978
|
+
runtime.finalizedToolCallIds.clear();
|
|
1742
1979
|
resetOutputState(context.output);
|
|
1743
1980
|
context.firstTokenTime = undefined;
|
|
1744
1981
|
}
|
|
@@ -1779,6 +2016,8 @@ async function tryRetryCodexProviderError(
|
|
|
1779
2016
|
runtime.currentItem = null;
|
|
1780
2017
|
runtime.currentBlock = null;
|
|
1781
2018
|
runtime.sawTerminalEvent = false;
|
|
2019
|
+
runtime.nativeOutputItems.length = 0;
|
|
2020
|
+
runtime.finalizedToolCallIds.clear();
|
|
1782
2021
|
resetOutputState(context.output);
|
|
1783
2022
|
context.firstTokenTime = undefined;
|
|
1784
2023
|
await scheduler.wait(CODEX_RETRY_DELAY_MS * runtime.providerRetryAttempt, {
|
|
@@ -1821,6 +2060,7 @@ function finalizeCodexResponse(
|
|
|
1821
2060
|
throw new Error("Codex response failed");
|
|
1822
2061
|
}
|
|
1823
2062
|
|
|
2063
|
+
removeTransientBlockIndices(output);
|
|
1824
2064
|
output.providerPayload = createOpenAIResponsesHistoryPayload(context.model.provider, runtime.nativeOutputItems);
|
|
1825
2065
|
output.duration = Date.now() - context.startTime;
|
|
1826
2066
|
if (completion.firstTokenTime) {
|
|
@@ -2290,10 +2530,15 @@ class CodexWebSocketConnection {
|
|
|
2290
2530
|
this.#socket = socket;
|
|
2291
2531
|
let settled = false;
|
|
2292
2532
|
let timeout: NodeJS.Timeout | undefined;
|
|
2533
|
+
const clearPending = () => {
|
|
2534
|
+
if (timeout) clearTimeout(timeout);
|
|
2535
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
2536
|
+
};
|
|
2293
2537
|
const onAbort = () => {
|
|
2294
2538
|
socket.close(1000, "aborted");
|
|
2295
2539
|
if (!settled) {
|
|
2296
2540
|
settled = true;
|
|
2541
|
+
clearPending();
|
|
2297
2542
|
reject(createCodexWebSocketTransportError("request was aborted"));
|
|
2298
2543
|
}
|
|
2299
2544
|
};
|
|
@@ -2304,17 +2549,16 @@ class CodexWebSocketConnection {
|
|
|
2304
2549
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
2305
2550
|
}
|
|
2306
2551
|
}
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
}, CODEX_WEBSOCKET_CONNECT_TIMEOUT_MS);
|
|
2552
|
+
if (!settled) {
|
|
2553
|
+
timeout = setTimeout(() => {
|
|
2554
|
+
socket.close(1000, "connect-timeout");
|
|
2555
|
+
if (!settled) {
|
|
2556
|
+
settled = true;
|
|
2557
|
+
clearPending();
|
|
2558
|
+
reject(createCodexWebSocketTransportError("connection timeout"));
|
|
2559
|
+
}
|
|
2560
|
+
}, CODEX_WEBSOCKET_CONNECT_TIMEOUT_MS);
|
|
2561
|
+
}
|
|
2318
2562
|
|
|
2319
2563
|
socket.onopen = event => {
|
|
2320
2564
|
if (!settled) {
|
|
@@ -2352,6 +2596,7 @@ class CodexWebSocketConnection {
|
|
|
2352
2596
|
};
|
|
2353
2597
|
socket.onmessage = event => {
|
|
2354
2598
|
try {
|
|
2599
|
+
if (!this.#activeRequest) return;
|
|
2355
2600
|
const text = typeof event.data === "string" ? event.data : Buffer.from(event.data).toString("utf-8");
|
|
2356
2601
|
if (!text) return;
|
|
2357
2602
|
const parsed = JSON.parse(text) as Record<string, unknown>;
|
|
@@ -2382,6 +2627,7 @@ class CodexWebSocketConnection {
|
|
|
2382
2627
|
request: Record<string, unknown>,
|
|
2383
2628
|
signal?: AbortSignal,
|
|
2384
2629
|
firstEventTimeoutMs?: number,
|
|
2630
|
+
idleTimeoutMs = this.#idleTimeoutMs,
|
|
2385
2631
|
): AsyncGenerator<Record<string, unknown>> {
|
|
2386
2632
|
if (!this.#socket || this.#socket.readyState !== WebSocket.OPEN) {
|
|
2387
2633
|
throw createCodexWebSocketTransportError("websocket connection is unavailable");
|
|
@@ -2405,14 +2651,23 @@ class CodexWebSocketConnection {
|
|
|
2405
2651
|
try {
|
|
2406
2652
|
this.#socket.send(JSON.stringify(request));
|
|
2407
2653
|
let sawFirstProgress = false;
|
|
2408
|
-
|
|
2654
|
+
const startedAt = Date.now();
|
|
2655
|
+
let lastProgressAt = startedAt;
|
|
2409
2656
|
while (true) {
|
|
2410
|
-
let timeoutMs =
|
|
2657
|
+
let timeoutMs =
|
|
2658
|
+
firstEventTimeoutMs === undefined ? undefined : firstEventTimeoutMs - (Date.now() - startedAt);
|
|
2411
2659
|
if (sawFirstProgress) {
|
|
2412
|
-
timeoutMs =
|
|
2660
|
+
timeoutMs = idleTimeoutMs - (Date.now() - lastProgressAt);
|
|
2413
2661
|
if (timeoutMs <= 0) {
|
|
2662
|
+
this.close("idle-timeout");
|
|
2414
2663
|
throw createCodexWebSocketTransportError("idle timeout waiting for websocket");
|
|
2415
2664
|
}
|
|
2665
|
+
} else if (timeoutMs !== undefined && timeoutMs <= 0) {
|
|
2666
|
+
this.close("first-event-timeout");
|
|
2667
|
+
throw createCodexWebSocketTransportError(
|
|
2668
|
+
"timeout waiting for first websocket event",
|
|
2669
|
+
STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE,
|
|
2670
|
+
);
|
|
2416
2671
|
}
|
|
2417
2672
|
const next = await this.#nextMessage(
|
|
2418
2673
|
timeoutMs,
|
|
@@ -2429,20 +2684,19 @@ class CodexWebSocketConnection {
|
|
|
2429
2684
|
sawFirstProgress = true;
|
|
2430
2685
|
lastProgressAt = Date.now();
|
|
2431
2686
|
}
|
|
2432
|
-
yield next;
|
|
2433
2687
|
const eventType = typeof next.type === "string" ? next.type : "";
|
|
2434
|
-
|
|
2688
|
+
const terminal =
|
|
2435
2689
|
eventType === "response.completed" ||
|
|
2436
2690
|
eventType === "response.done" ||
|
|
2437
2691
|
eventType === "response.incomplete" ||
|
|
2438
2692
|
eventType === "response.failed" ||
|
|
2439
|
-
eventType === "error"
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
}
|
|
2693
|
+
eventType === "error";
|
|
2694
|
+
yield next;
|
|
2695
|
+
if (terminal) break;
|
|
2443
2696
|
}
|
|
2444
2697
|
} finally {
|
|
2445
2698
|
this.#activeRequest = false;
|
|
2699
|
+
this.#queue.length = 0;
|
|
2446
2700
|
if (signal) {
|
|
2447
2701
|
signal.removeEventListener("abort", onAbort);
|
|
2448
2702
|
}
|
|
@@ -2485,9 +2739,9 @@ class CodexWebSocketConnection {
|
|
|
2485
2739
|
await promise;
|
|
2486
2740
|
if (timeout) clearTimeout(timeout);
|
|
2487
2741
|
if (timedOut && this.#queue.length === 0) {
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2742
|
+
this.close(
|
|
2743
|
+
providerCode === STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE ? "first-event-timeout" : "idle-timeout",
|
|
2744
|
+
);
|
|
2491
2745
|
return createCodexWebSocketTransportError(timeoutReason, providerCode);
|
|
2492
2746
|
}
|
|
2493
2747
|
}
|
|
@@ -2590,7 +2844,12 @@ async function openCodexWebSocketEventStream(
|
|
|
2590
2844
|
firstEventTimeoutMs?: number,
|
|
2591
2845
|
): Promise<AsyncGenerator<Record<string, unknown>>> {
|
|
2592
2846
|
const connection = await getOrCreateCodexWebSocketConnection(state, url, headers, signal, options);
|
|
2593
|
-
return connection.streamRequest(
|
|
2847
|
+
return connection.streamRequest(
|
|
2848
|
+
request,
|
|
2849
|
+
signal,
|
|
2850
|
+
firstEventTimeoutMs,
|
|
2851
|
+
getCodexWebSocketIdleTimeoutMs(options?.streamIdleTimeoutMs),
|
|
2852
|
+
);
|
|
2594
2853
|
}
|
|
2595
2854
|
|
|
2596
2855
|
function createCodexHeaders(
|
|
@@ -2710,8 +2969,27 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
|
|
2710
2969
|
// `function_call_output` (OpenAI rejects mismatched pairs).
|
|
2711
2970
|
const customCallIds = new Set<string>();
|
|
2712
2971
|
const knownCallIds = new Set<string>();
|
|
2972
|
+
// Consecutive tool results are batched into one append call so every output
|
|
2973
|
+
// of the turn stays contiguous before the collected image user message;
|
|
2974
|
+
// per-result image user messages interleave with sibling outputs and break
|
|
2975
|
+
// tool_use→tool_result adjacency through Anthropic-translating proxies (#4807).
|
|
2976
|
+
let pendingToolResults: ToolResultMessage[] = [];
|
|
2977
|
+
const flushPendingToolResults = (): void => {
|
|
2978
|
+
if (pendingToolResults.length === 0) return;
|
|
2979
|
+
appendResponsesToolResultMessages(messages, pendingToolResults, model, false, knownCallIds, customCallIds);
|
|
2980
|
+
pendingToolResults = [];
|
|
2981
|
+
};
|
|
2713
2982
|
|
|
2714
2983
|
for (const msg of transformedMessages) {
|
|
2984
|
+
if (msg.role === "toolResult") {
|
|
2985
|
+
pendingToolResults.push(msg);
|
|
2986
|
+
msgIndex += 1;
|
|
2987
|
+
continue;
|
|
2988
|
+
}
|
|
2989
|
+
// Flush pending tool results before any non-tool-result message so the
|
|
2990
|
+
// batched outputs (and their collected image user message) stay directly
|
|
2991
|
+
// after their assistant tool-call turn, never behind later turns (#4807).
|
|
2992
|
+
flushPendingToolResults();
|
|
2715
2993
|
if (msg.role === "user" || msg.role === "developer") {
|
|
2716
2994
|
const providerPayload = (msg as { providerPayload?: AssistantMessage["providerPayload"] }).providerPayload;
|
|
2717
2995
|
const historyItems = getOpenAIResponsesHistoryItems(providerPayload, model.provider);
|
|
@@ -2780,15 +3058,9 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
|
|
2780
3058
|
messages.push(...outputItems);
|
|
2781
3059
|
}
|
|
2782
3060
|
msgIndex += 1;
|
|
2783
|
-
continue;
|
|
2784
3061
|
}
|
|
2785
|
-
|
|
2786
|
-
if (msg.role === "toolResult") {
|
|
2787
|
-
appendResponsesToolResultMessages(messages, msg, model, false, knownCallIds, customCallIds);
|
|
2788
|
-
}
|
|
2789
|
-
|
|
2790
|
-
msgIndex += 1;
|
|
2791
3062
|
}
|
|
3063
|
+
flushPendingToolResults();
|
|
2792
3064
|
|
|
2793
3065
|
return messages;
|
|
2794
3066
|
}
|
|
@@ -2897,12 +3169,20 @@ function getCodexEventErrorMessage(rawEvent: Record<string, unknown>): string {
|
|
|
2897
3169
|
class CodexProviderStreamError extends Error {
|
|
2898
3170
|
readonly retryable: boolean;
|
|
2899
3171
|
readonly code?: string;
|
|
2900
|
-
|
|
2901
|
-
|
|
3172
|
+
/**
|
|
3173
|
+
* Provider-supplied message, before display formatting appends `code=`/`status=`
|
|
3174
|
+
* metadata. Classification must read this, never `message`: the formatted string
|
|
3175
|
+
* mixes the code into the prose and would let `code=invalid_request_error`
|
|
3176
|
+
* satisfy a message pattern the provider never actually sent.
|
|
3177
|
+
*/
|
|
3178
|
+
readonly providerMessage: string;
|
|
3179
|
+
|
|
3180
|
+
constructor(message: string, retryable: boolean, code: string | undefined, providerMessage: string) {
|
|
2902
3181
|
super(message);
|
|
2903
3182
|
this.name = "CodexProviderStreamError";
|
|
2904
3183
|
this.retryable = retryable;
|
|
2905
3184
|
this.code = code;
|
|
3185
|
+
this.providerMessage = providerMessage;
|
|
2906
3186
|
}
|
|
2907
3187
|
}
|
|
2908
3188
|
|
|
@@ -2928,7 +3208,12 @@ function createCodexProviderStreamError(rawEvent: Record<string, unknown>): Code
|
|
|
2928
3208
|
typeof rawEvent.type === "string" && rawEvent.type === "error"
|
|
2929
3209
|
? formatCodexErrorEvent(rawEvent, code, message)
|
|
2930
3210
|
: (formatCodexFailure(rawEvent) ?? "Codex response failed");
|
|
2931
|
-
return new CodexProviderStreamError(
|
|
3211
|
+
return new CodexProviderStreamError(
|
|
3212
|
+
formattedMessage,
|
|
3213
|
+
isRetryableCodexFailureEvent(rawEvent),
|
|
3214
|
+
code || undefined,
|
|
3215
|
+
message,
|
|
3216
|
+
);
|
|
2932
3217
|
}
|
|
2933
3218
|
|
|
2934
3219
|
function isRetryableCodexProviderError(error: unknown): boolean {
|