@f5-sales-demo/pi-ai 19.51.2
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 +1997 -0
- package/README.md +1160 -0
- package/package.json +135 -0
- package/src/api-registry.ts +95 -0
- package/src/auth-storage.ts +2694 -0
- package/src/cli.ts +493 -0
- package/src/index.ts +42 -0
- package/src/model-cache.ts +97 -0
- package/src/model-manager.ts +349 -0
- package/src/model-thinking.ts +561 -0
- package/src/models.json +49439 -0
- package/src/models.json.d.ts +9 -0
- package/src/models.ts +56 -0
- package/src/prompts/turn-aborted-guidance.md +4 -0
- package/src/provider-details.ts +81 -0
- package/src/provider-models/descriptors.ts +285 -0
- package/src/provider-models/google.ts +90 -0
- package/src/provider-models/index.ts +4 -0
- package/src/provider-models/openai-compat.ts +2074 -0
- package/src/provider-models/special.ts +106 -0
- package/src/providers/amazon-bedrock.ts +706 -0
- package/src/providers/anthropic.ts +1682 -0
- package/src/providers/azure-openai-responses.ts +391 -0
- package/src/providers/cursor/gen/agent_pb.ts +15274 -0
- package/src/providers/cursor/proto/agent.proto +3526 -0
- package/src/providers/cursor/proto/buf.gen.yaml +6 -0
- package/src/providers/cursor/proto/buf.yaml +17 -0
- package/src/providers/cursor.ts +2218 -0
- package/src/providers/github-copilot-headers.ts +140 -0
- package/src/providers/gitlab-duo.ts +381 -0
- package/src/providers/google-gemini-cli.ts +1133 -0
- package/src/providers/google-shared.ts +354 -0
- package/src/providers/google-vertex.ts +436 -0
- package/src/providers/google.ts +381 -0
- package/src/providers/kimi.ts +151 -0
- package/src/providers/openai-codex/constants.ts +43 -0
- package/src/providers/openai-codex/request-transformer.ts +158 -0
- package/src/providers/openai-codex/response-handler.ts +81 -0
- package/src/providers/openai-codex-responses.ts +2345 -0
- package/src/providers/openai-completions-compat.ts +159 -0
- package/src/providers/openai-completions.ts +1290 -0
- package/src/providers/openai-responses-shared.ts +452 -0
- package/src/providers/openai-responses.ts +519 -0
- package/src/providers/register-builtins.ts +329 -0
- package/src/providers/synthetic.ts +154 -0
- package/src/providers/transform-messages.ts +234 -0
- package/src/rate-limit-utils.ts +84 -0
- package/src/stream.ts +728 -0
- package/src/types.ts +546 -0
- package/src/usage/claude.ts +337 -0
- package/src/usage/gemini.ts +248 -0
- package/src/usage/github-copilot.ts +421 -0
- package/src/usage/google-antigravity.ts +200 -0
- package/src/usage/kimi.ts +286 -0
- package/src/usage/minimax-code.ts +31 -0
- package/src/usage/openai-codex.ts +387 -0
- package/src/usage/zai.ts +247 -0
- package/src/usage.ts +130 -0
- package/src/utils/abort.ts +36 -0
- package/src/utils/anthropic-auth.ts +293 -0
- package/src/utils/discovery/antigravity.ts +261 -0
- package/src/utils/discovery/codex.ts +371 -0
- package/src/utils/discovery/cursor.ts +306 -0
- package/src/utils/discovery/gemini.ts +248 -0
- package/src/utils/discovery/index.ts +5 -0
- package/src/utils/discovery/openai-compatible.ts +224 -0
- package/src/utils/event-stream.ts +209 -0
- package/src/utils/http-inspector.ts +165 -0
- package/src/utils/idle-iterator.ts +176 -0
- package/src/utils/json-parse.ts +28 -0
- package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
- package/src/utils/oauth/anthropic.ts +134 -0
- package/src/utils/oauth/api-key-validation.ts +92 -0
- package/src/utils/oauth/callback-server.ts +276 -0
- package/src/utils/oauth/cerebras.ts +59 -0
- package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
- package/src/utils/oauth/cursor.ts +157 -0
- package/src/utils/oauth/github-copilot.ts +358 -0
- package/src/utils/oauth/gitlab-duo.ts +123 -0
- package/src/utils/oauth/google-antigravity.ts +275 -0
- package/src/utils/oauth/google-gemini-cli.ts +334 -0
- package/src/utils/oauth/huggingface.ts +62 -0
- package/src/utils/oauth/index.ts +512 -0
- package/src/utils/oauth/kagi.ts +47 -0
- package/src/utils/oauth/kilo.ts +87 -0
- package/src/utils/oauth/kimi.ts +251 -0
- package/src/utils/oauth/litellm.ts +81 -0
- package/src/utils/oauth/lm-studio.ts +40 -0
- package/src/utils/oauth/minimax-code.ts +78 -0
- package/src/utils/oauth/moonshot.ts +59 -0
- package/src/utils/oauth/nanogpt.ts +51 -0
- package/src/utils/oauth/nvidia.ts +70 -0
- package/src/utils/oauth/oauth.html +199 -0
- package/src/utils/oauth/ollama.ts +47 -0
- package/src/utils/oauth/openai-codex.ts +190 -0
- package/src/utils/oauth/opencode.ts +49 -0
- package/src/utils/oauth/parallel.ts +46 -0
- package/src/utils/oauth/perplexity.ts +200 -0
- package/src/utils/oauth/pkce.ts +18 -0
- package/src/utils/oauth/qianfan.ts +58 -0
- package/src/utils/oauth/qwen-portal.ts +60 -0
- package/src/utils/oauth/synthetic.ts +60 -0
- package/src/utils/oauth/tavily.ts +46 -0
- package/src/utils/oauth/together.ts +59 -0
- package/src/utils/oauth/types.ts +89 -0
- package/src/utils/oauth/venice.ts +59 -0
- package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
- package/src/utils/oauth/vllm.ts +40 -0
- package/src/utils/oauth/xiaomi.ts +88 -0
- package/src/utils/oauth/zai.ts +60 -0
- package/src/utils/oauth/zenmux.ts +51 -0
- package/src/utils/overflow.ts +134 -0
- package/src/utils/retry-after.ts +110 -0
- package/src/utils/retry.ts +93 -0
- package/src/utils/schema/CONSTRAINTS.md +160 -0
- package/src/utils/schema/adapt.ts +20 -0
- package/src/utils/schema/compatibility.ts +397 -0
- package/src/utils/schema/dereference.ts +93 -0
- package/src/utils/schema/equality.ts +93 -0
- package/src/utils/schema/fields.ts +147 -0
- package/src/utils/schema/index.ts +9 -0
- package/src/utils/schema/normalize-cca.ts +479 -0
- package/src/utils/schema/sanitize-google.ts +212 -0
- package/src/utils/schema/strict-mode.ts +385 -0
- package/src/utils/schema/types.ts +5 -0
- package/src/utils/tool-choice.ts +81 -0
- package/src/utils/validation.ts +664 -0
- package/src/utils.ts +147 -0
|
@@ -0,0 +1,2345 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import {
|
|
3
|
+
$env,
|
|
4
|
+
$flag,
|
|
5
|
+
abortableSleep,
|
|
6
|
+
asRecord,
|
|
7
|
+
logger,
|
|
8
|
+
readSseJson,
|
|
9
|
+
structuredCloneJSON,
|
|
10
|
+
} from "@f5xc-salesdemos/pi-utils";
|
|
11
|
+
import type OpenAI from "openai";
|
|
12
|
+
import type {
|
|
13
|
+
ResponseFunctionToolCall,
|
|
14
|
+
ResponseInput,
|
|
15
|
+
ResponseInputContent,
|
|
16
|
+
ResponseInputImage,
|
|
17
|
+
ResponseInputText,
|
|
18
|
+
ResponseOutputMessage,
|
|
19
|
+
ResponseReasoningItem,
|
|
20
|
+
} from "openai/resources/responses/responses";
|
|
21
|
+
import packageJson from "../../package.json" with { type: "json" };
|
|
22
|
+
import { calculateCost } from "../models";
|
|
23
|
+
import { isUsageLimitError } from "../rate-limit-utils";
|
|
24
|
+
import { getEnvApiKey } from "../stream";
|
|
25
|
+
import {
|
|
26
|
+
type Api,
|
|
27
|
+
type AssistantMessage,
|
|
28
|
+
type Context,
|
|
29
|
+
isSpecialServiceTier,
|
|
30
|
+
type Model,
|
|
31
|
+
type ProviderSessionState,
|
|
32
|
+
type ServiceTier,
|
|
33
|
+
type StreamFunction,
|
|
34
|
+
type StreamOptions,
|
|
35
|
+
type TextContent,
|
|
36
|
+
type ThinkingContent,
|
|
37
|
+
type Tool,
|
|
38
|
+
type ToolCall,
|
|
39
|
+
type ToolChoice,
|
|
40
|
+
} from "../types";
|
|
41
|
+
import {
|
|
42
|
+
createOpenAIResponsesHistoryPayload,
|
|
43
|
+
getOpenAIResponsesHistoryItems,
|
|
44
|
+
getOpenAIResponsesHistoryPayload,
|
|
45
|
+
normalizeResponsesToolCallId,
|
|
46
|
+
} from "../utils";
|
|
47
|
+
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
48
|
+
import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
|
|
49
|
+
import { getOpenAIStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator";
|
|
50
|
+
import { parseStreamingJson } from "../utils/json-parse";
|
|
51
|
+
import { adaptSchemaForStrict, NO_STRICT } from "../utils/schema";
|
|
52
|
+
import {
|
|
53
|
+
CODEX_BASE_URL,
|
|
54
|
+
getCodexAccountId,
|
|
55
|
+
OPENAI_HEADER_VALUES,
|
|
56
|
+
OPENAI_HEADERS,
|
|
57
|
+
URL_PATHS,
|
|
58
|
+
} from "./openai-codex/constants";
|
|
59
|
+
import {
|
|
60
|
+
type CodexRequestOptions,
|
|
61
|
+
type InputItem,
|
|
62
|
+
type RequestBody,
|
|
63
|
+
transformRequestBody,
|
|
64
|
+
} from "./openai-codex/request-transformer";
|
|
65
|
+
import { parseCodexError } from "./openai-codex/response-handler";
|
|
66
|
+
import { encodeTextSignatureV1, mapOpenAIResponsesStopReason, parseTextSignature } from "./openai-responses-shared";
|
|
67
|
+
import { transformMessages } from "./transform-messages";
|
|
68
|
+
|
|
69
|
+
export interface OpenAICodexResponsesOptions extends StreamOptions {
|
|
70
|
+
reasoning?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
71
|
+
reasoningSummary?: "auto" | "concise" | "detailed" | null;
|
|
72
|
+
textVerbosity?: "low" | "medium" | "high";
|
|
73
|
+
include?: string[];
|
|
74
|
+
codexMode?: boolean;
|
|
75
|
+
toolChoice?: ToolChoice;
|
|
76
|
+
preferWebsockets?: boolean;
|
|
77
|
+
serviceTier?: ServiceTier;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const CODEX_INSTRUCTIONS = `You are an expert coding assistant operating inside pi, a coding agent harness.`;
|
|
81
|
+
|
|
82
|
+
export interface CodexSystemPrompt {
|
|
83
|
+
instructions: string;
|
|
84
|
+
developerMessages: string[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function buildCodexSystemPrompt(args: { userSystemPrompt?: string }): CodexSystemPrompt {
|
|
88
|
+
const { userSystemPrompt } = args;
|
|
89
|
+
const developerMessages: string[] = [];
|
|
90
|
+
|
|
91
|
+
if (userSystemPrompt && userSystemPrompt.trim().length > 0) {
|
|
92
|
+
developerMessages.push(userSystemPrompt.trim());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
instructions: CODEX_INSTRUCTIONS,
|
|
97
|
+
developerMessages,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const CODEX_DEBUG = $flag("PI_CODEX_DEBUG");
|
|
102
|
+
const CODEX_MAX_RETRIES = 5;
|
|
103
|
+
const CODEX_RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);
|
|
104
|
+
const CODEX_RETRY_DELAY_MS = 500;
|
|
105
|
+
const CODEX_WEBSOCKET_CONNECT_TIMEOUT_MS = 10000;
|
|
106
|
+
const CODEX_WEBSOCKET_IDLE_TIMEOUT_MS = 300000;
|
|
107
|
+
const CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS = 15000;
|
|
108
|
+
const CODEX_WEBSOCKET_RETRY_BUDGET = CODEX_MAX_RETRIES;
|
|
109
|
+
const CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX = "Codex websocket transport error";
|
|
110
|
+
const CODEX_RETRYABLE_EVENT_CODES = new Set(["model_error", "server_error", "internal_error"]);
|
|
111
|
+
const CODEX_RETRYABLE_EVENT_MESSAGE =
|
|
112
|
+
/processing your request|retry your request|temporar(?:y|ily)|overloaded|service.?unavailable|internal error|server error/i;
|
|
113
|
+
const CODEX_PROVIDER_SESSION_STATE_KEY = "openai-codex-responses";
|
|
114
|
+
const X_CODEX_TURN_STATE_HEADER = "x-codex-turn-state";
|
|
115
|
+
const X_MODELS_ETAG_HEADER = "x-models-etag";
|
|
116
|
+
const X_REASONING_INCLUDED_HEADER = "x-reasoning-included";
|
|
117
|
+
/** Connection-level websocket failures that should immediately fall back to SSE without retrying. */
|
|
118
|
+
const CODEX_WEBSOCKET_FATAL_PATTERNS = ["websocket error:", "websocket closed before open", "connection timeout"];
|
|
119
|
+
/** Max total time to spend retrying 429s with server-provided delays (5 minutes). */
|
|
120
|
+
const CODEX_RATE_LIMIT_BUDGET_MS = 5 * 60 * 1000;
|
|
121
|
+
|
|
122
|
+
type CodexTransport = "sse" | "websocket";
|
|
123
|
+
type CodexEventItem = ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall;
|
|
124
|
+
type CodexOutputBlock = ThinkingContent | TextContent | (ToolCall & { partialJson: string });
|
|
125
|
+
|
|
126
|
+
type CodexWebSocketSessionState = {
|
|
127
|
+
disableWebsocket: boolean;
|
|
128
|
+
lastRequest?: RequestBody;
|
|
129
|
+
lastResponseId?: string;
|
|
130
|
+
canAppend: boolean;
|
|
131
|
+
turnState?: string;
|
|
132
|
+
modelsEtag?: string;
|
|
133
|
+
reasoningIncluded?: boolean;
|
|
134
|
+
connection?: CodexWebSocketConnection;
|
|
135
|
+
lastTransport?: CodexTransport;
|
|
136
|
+
fallbackCount: number;
|
|
137
|
+
lastFallbackAt?: number;
|
|
138
|
+
prewarmed: boolean;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
interface CodexProviderSessionState extends ProviderSessionState {
|
|
142
|
+
webSocketSessions: Map<string, CodexWebSocketSessionState>;
|
|
143
|
+
webSocketPublicToPrivate: Map<string, string>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
interface CodexRequestContext {
|
|
147
|
+
apiKey: string;
|
|
148
|
+
accountId: string;
|
|
149
|
+
baseUrl: string;
|
|
150
|
+
url: string;
|
|
151
|
+
requestHeaders: Record<string, string>;
|
|
152
|
+
providerSessionState?: CodexProviderSessionState;
|
|
153
|
+
websocketState?: CodexWebSocketSessionState;
|
|
154
|
+
transformedBody: RequestBody;
|
|
155
|
+
rawRequestDump: RawHttpRequestDump;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface CodexRequestSetup {
|
|
159
|
+
requestSignal: AbortSignal;
|
|
160
|
+
wrapCodexSseStream: (source: AsyncGenerator<Record<string, unknown>>) => AsyncGenerator<Record<string, unknown>>;
|
|
161
|
+
requestAbortController: AbortController;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface CodexStreamRuntime {
|
|
165
|
+
eventStream: AsyncGenerator<Record<string, unknown>>;
|
|
166
|
+
requestBodyForState: RequestBody;
|
|
167
|
+
transport: CodexTransport;
|
|
168
|
+
websocketState?: CodexWebSocketSessionState;
|
|
169
|
+
currentItem: CodexEventItem | null;
|
|
170
|
+
currentBlock: CodexOutputBlock | null;
|
|
171
|
+
nativeOutputItems: Array<Record<string, unknown>>;
|
|
172
|
+
websocketStreamRetries: number;
|
|
173
|
+
providerRetryAttempt: number;
|
|
174
|
+
sawTerminalEvent: boolean;
|
|
175
|
+
canSafelyReplayWebsocketOverSse: boolean;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
interface CodexStreamProcessingContext {
|
|
179
|
+
model: Model<"openai-codex-responses">;
|
|
180
|
+
output: AssistantMessage;
|
|
181
|
+
stream: AssistantMessageEventStream;
|
|
182
|
+
options: OpenAICodexResponsesOptions | undefined;
|
|
183
|
+
requestSetup: CodexRequestSetup;
|
|
184
|
+
requestContext: CodexRequestContext;
|
|
185
|
+
startTime: number;
|
|
186
|
+
firstTokenTime?: number;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
interface CodexStreamCompletion {
|
|
190
|
+
firstTokenTime?: number;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parseCodexNonNegativeInteger(value: string | undefined, fallback: number): number {
|
|
194
|
+
if (!value) return fallback;
|
|
195
|
+
const parsed = Number(value);
|
|
196
|
+
if (!Number.isFinite(parsed) || parsed < 0) return fallback;
|
|
197
|
+
return Math.trunc(parsed);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function parseCodexPositiveInteger(value: string | undefined, fallback: number): number {
|
|
201
|
+
if (!value) return fallback;
|
|
202
|
+
const parsed = Number(value);
|
|
203
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
204
|
+
return Math.trunc(parsed);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isCodexWebSocketEnvEnabled(): boolean {
|
|
208
|
+
return $flag("PI_CODEX_WEBSOCKET");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function getCodexWebSocketRetryBudget(): number {
|
|
212
|
+
return parseCodexNonNegativeInteger($env.PI_CODEX_WEBSOCKET_RETRY_BUDGET, CODEX_WEBSOCKET_RETRY_BUDGET);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function getCodexWebSocketRetryDelayMs(retry: number): number {
|
|
216
|
+
const baseDelay = parseCodexPositiveInteger($env.PI_CODEX_WEBSOCKET_RETRY_DELAY_MS, CODEX_RETRY_DELAY_MS);
|
|
217
|
+
return baseDelay * Math.max(1, retry);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function getCodexWebSocketIdleTimeoutMs(): number {
|
|
221
|
+
return parseCodexPositiveInteger($env.PI_CODEX_WEBSOCKET_IDLE_TIMEOUT_MS, CODEX_WEBSOCKET_IDLE_TIMEOUT_MS);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function getCodexWebSocketFirstEventTimeoutMs(): number {
|
|
225
|
+
return parseCodexPositiveInteger(
|
|
226
|
+
$env.PI_CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS,
|
|
227
|
+
Math.min(CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS, getCodexWebSocketIdleTimeoutMs()),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function createCodexProviderSessionState(): CodexProviderSessionState {
|
|
232
|
+
const state: CodexProviderSessionState = {
|
|
233
|
+
webSocketSessions: new Map(),
|
|
234
|
+
webSocketPublicToPrivate: new Map(),
|
|
235
|
+
close: () => {
|
|
236
|
+
for (const session of state.webSocketSessions.values()) {
|
|
237
|
+
session.connection?.close("session_disposed");
|
|
238
|
+
}
|
|
239
|
+
state.webSocketSessions.clear();
|
|
240
|
+
state.webSocketPublicToPrivate.clear();
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
return state;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function getCodexProviderSessionState(
|
|
247
|
+
providerSessionState: Map<string, ProviderSessionState> | undefined,
|
|
248
|
+
): CodexProviderSessionState | undefined {
|
|
249
|
+
if (!providerSessionState) return undefined;
|
|
250
|
+
const existing = providerSessionState.get(CODEX_PROVIDER_SESSION_STATE_KEY) as CodexProviderSessionState | undefined;
|
|
251
|
+
if (existing) return existing;
|
|
252
|
+
const created = createCodexProviderSessionState();
|
|
253
|
+
providerSessionState.set(CODEX_PROVIDER_SESSION_STATE_KEY, created);
|
|
254
|
+
return created;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function createCodexWebSocketTransportError(message: string): Error {
|
|
258
|
+
return new Error(`${CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX}: ${message}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isCodexWebSocketFatalError(error: Error): boolean {
|
|
262
|
+
const msg = error.message.toLowerCase();
|
|
263
|
+
return CODEX_WEBSOCKET_FATAL_PATTERNS.some(pattern => msg.includes(pattern.toLowerCase()));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function isCodexWebSocketTransportError(error: unknown): boolean {
|
|
267
|
+
if (!(error instanceof Error)) return false;
|
|
268
|
+
return error.message.startsWith(CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function isCodexWebSocketRetryableStreamError(error: unknown): boolean {
|
|
272
|
+
if (!(error instanceof Error) || !isCodexWebSocketTransportError(error)) return false;
|
|
273
|
+
const message = error.message.toLowerCase();
|
|
274
|
+
return (
|
|
275
|
+
message.includes("websocket closed (") ||
|
|
276
|
+
message.includes("websocket closed before response completion") ||
|
|
277
|
+
message.includes("websocket connection is unavailable") ||
|
|
278
|
+
message.includes("idle timeout waiting for websocket") ||
|
|
279
|
+
message.includes("timeout waiting for first websocket event") ||
|
|
280
|
+
message.includes("syntaxerror") ||
|
|
281
|
+
message.includes("json")
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function toCodexHeaderRecord(value: unknown): Record<string, string> | null {
|
|
286
|
+
if (!value || typeof value !== "object") return null;
|
|
287
|
+
const headers: Record<string, string> = {};
|
|
288
|
+
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
289
|
+
if (typeof entry === "string") {
|
|
290
|
+
headers[key] = entry;
|
|
291
|
+
} else if (Array.isArray(entry) && entry.every(item => typeof item === "string")) {
|
|
292
|
+
headers[key] = entry.join(",");
|
|
293
|
+
} else if (typeof entry === "number" || typeof entry === "boolean") {
|
|
294
|
+
headers[key] = String(entry);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return Object.keys(headers).length > 0 ? headers : null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function toCodexHeaders(value: unknown): Headers | undefined {
|
|
301
|
+
if (!value) return undefined;
|
|
302
|
+
if (value instanceof Headers) return value;
|
|
303
|
+
if (Array.isArray(value)) {
|
|
304
|
+
try {
|
|
305
|
+
return new Headers(value as Array<[string, string]>);
|
|
306
|
+
} catch {
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const record = toCodexHeaderRecord(value);
|
|
311
|
+
if (!record) return undefined;
|
|
312
|
+
return new Headers(record);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function updateCodexSessionMetadataFromHeaders(
|
|
316
|
+
state: CodexWebSocketSessionState | undefined,
|
|
317
|
+
headers: Headers | Record<string, string> | null | undefined,
|
|
318
|
+
): void {
|
|
319
|
+
if (!state || !headers) return;
|
|
320
|
+
const resolvedHeaders = headers instanceof Headers ? headers : new Headers(headers);
|
|
321
|
+
const turnState = resolvedHeaders.get(X_CODEX_TURN_STATE_HEADER);
|
|
322
|
+
if (turnState && turnState.length > 0) {
|
|
323
|
+
state.turnState = turnState;
|
|
324
|
+
}
|
|
325
|
+
const modelsEtag = resolvedHeaders.get(X_MODELS_ETAG_HEADER);
|
|
326
|
+
if (modelsEtag && modelsEtag.length > 0) {
|
|
327
|
+
state.modelsEtag = modelsEtag;
|
|
328
|
+
}
|
|
329
|
+
const reasoningIncluded = resolvedHeaders.get(X_REASONING_INCLUDED_HEADER);
|
|
330
|
+
if (reasoningIncluded !== null) {
|
|
331
|
+
const normalized = reasoningIncluded.trim().toLowerCase();
|
|
332
|
+
state.reasoningIncluded = normalized.length === 0 ? true : normalized !== "false";
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function extractCodexWebSocketHandshakeHeaders(socket: WebSocket, openEvent?: Event): Headers | undefined {
|
|
337
|
+
const eventRecord = openEvent as Record<string, unknown> | undefined;
|
|
338
|
+
const eventResponse = eventRecord?.response as Record<string, unknown> | undefined;
|
|
339
|
+
const socketRecord = socket as unknown as Record<string, unknown>;
|
|
340
|
+
const socketResponse = socketRecord.response as Record<string, unknown> | undefined;
|
|
341
|
+
const socketHandshake = socketRecord.handshake as Record<string, unknown> | undefined;
|
|
342
|
+
return (
|
|
343
|
+
toCodexHeaders(eventRecord?.responseHeaders) ??
|
|
344
|
+
toCodexHeaders(eventRecord?.headers) ??
|
|
345
|
+
toCodexHeaders(eventResponse?.headers) ??
|
|
346
|
+
toCodexHeaders(socketRecord.responseHeaders) ??
|
|
347
|
+
toCodexHeaders(socketRecord.handshakeHeaders) ??
|
|
348
|
+
toCodexHeaders(socketResponse?.headers) ??
|
|
349
|
+
toCodexHeaders(socketHandshake?.headers)
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function normalizeCodexToolChoice(choice: ToolChoice | undefined): string | Record<string, unknown> | undefined {
|
|
354
|
+
if (!choice) return undefined;
|
|
355
|
+
if (typeof choice === "string") return choice;
|
|
356
|
+
if (choice.type === "function") {
|
|
357
|
+
if ("function" in choice && choice.function?.name) {
|
|
358
|
+
return { type: "function", name: choice.function.name };
|
|
359
|
+
}
|
|
360
|
+
if ("name" in choice && choice.name) {
|
|
361
|
+
return { type: "function", name: choice.name };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (choice.type === "tool" && choice.name) {
|
|
365
|
+
return { type: "function", name: choice.name };
|
|
366
|
+
}
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function createEmptyUsage(): AssistantMessage["usage"] {
|
|
371
|
+
return {
|
|
372
|
+
input: 0,
|
|
373
|
+
output: 0,
|
|
374
|
+
cacheRead: 0,
|
|
375
|
+
cacheWrite: 0,
|
|
376
|
+
totalTokens: 0,
|
|
377
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function createAssistantOutput(model: Model<"openai-codex-responses">): AssistantMessage {
|
|
382
|
+
return {
|
|
383
|
+
role: "assistant",
|
|
384
|
+
content: [],
|
|
385
|
+
api: "openai-codex-responses" as Api,
|
|
386
|
+
provider: model.provider,
|
|
387
|
+
model: model.id,
|
|
388
|
+
usage: createEmptyUsage(),
|
|
389
|
+
stopReason: "stop",
|
|
390
|
+
timestamp: Date.now(),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function resetOutputState(output: AssistantMessage): void {
|
|
395
|
+
output.content.length = 0;
|
|
396
|
+
output.usage = createEmptyUsage();
|
|
397
|
+
output.stopReason = "stop";
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function removeTransientBlockIndices(output: AssistantMessage): void {
|
|
401
|
+
for (const block of output.content) {
|
|
402
|
+
delete (block as { index?: number }).index;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function createRequestSetup(options: OpenAICodexResponsesOptions | undefined): CodexRequestSetup {
|
|
407
|
+
const requestAbortController = new AbortController();
|
|
408
|
+
const requestSignal = options?.signal
|
|
409
|
+
? AbortSignal.any([options.signal, requestAbortController.signal])
|
|
410
|
+
: requestAbortController.signal;
|
|
411
|
+
const wrapCodexSseStream = (
|
|
412
|
+
source: AsyncGenerator<Record<string, unknown>>,
|
|
413
|
+
): AsyncGenerator<Record<string, unknown>> =>
|
|
414
|
+
iterateWithIdleTimeout(source, {
|
|
415
|
+
idleTimeoutMs: getOpenAIStreamIdleTimeoutMs(),
|
|
416
|
+
errorMessage: "OpenAI Codex SSE stream stalled while waiting for the next event",
|
|
417
|
+
onIdle: () => requestAbortController.abort(),
|
|
418
|
+
});
|
|
419
|
+
return { requestAbortController, requestSignal, wrapCodexSseStream };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function buildCodexRequestContext(
|
|
423
|
+
model: Model<"openai-codex-responses">,
|
|
424
|
+
context: Context,
|
|
425
|
+
options: OpenAICodexResponsesOptions | undefined,
|
|
426
|
+
output: AssistantMessage,
|
|
427
|
+
): Promise<CodexRequestContext> {
|
|
428
|
+
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
429
|
+
if (!apiKey) {
|
|
430
|
+
throw new Error(`No API key for provider: ${model.provider}`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const accountId = getAccountId(apiKey);
|
|
434
|
+
const baseUrl = model.baseUrl || CODEX_BASE_URL;
|
|
435
|
+
const baseWithSlash = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
436
|
+
const url = rewriteUrlForCodex(new URL(URL_PATHS.RESPONSES.slice(1), baseWithSlash).toString());
|
|
437
|
+
const transformedBody = await buildTransformedCodexRequestBody(model, context, options);
|
|
438
|
+
options?.onPayload?.(transformedBody);
|
|
439
|
+
|
|
440
|
+
const requestHeaders = { ...(model.headers ?? {}), ...(options?.headers ?? {}) };
|
|
441
|
+
const rawRequestDump: RawHttpRequestDump = {
|
|
442
|
+
provider: model.provider,
|
|
443
|
+
api: output.api,
|
|
444
|
+
model: model.id,
|
|
445
|
+
method: "POST",
|
|
446
|
+
url,
|
|
447
|
+
body: transformedBody,
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
|
|
451
|
+
const sessionKey = getCodexWebSocketSessionKey(options?.sessionId, model, accountId, baseUrl);
|
|
452
|
+
const publicSessionKey = getCodexPublicSessionKey(options?.sessionId, model, baseUrl);
|
|
453
|
+
if (sessionKey && publicSessionKey) {
|
|
454
|
+
providerSessionState?.webSocketPublicToPrivate.set(publicSessionKey, sessionKey);
|
|
455
|
+
}
|
|
456
|
+
const websocketState =
|
|
457
|
+
sessionKey && providerSessionState ? getCodexWebSocketSessionState(sessionKey, providerSessionState) : undefined;
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
apiKey,
|
|
461
|
+
accountId,
|
|
462
|
+
baseUrl,
|
|
463
|
+
url,
|
|
464
|
+
requestHeaders,
|
|
465
|
+
providerSessionState,
|
|
466
|
+
websocketState,
|
|
467
|
+
transformedBody,
|
|
468
|
+
rawRequestDump,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function buildTransformedCodexRequestBody(
|
|
473
|
+
model: Model<"openai-codex-responses">,
|
|
474
|
+
context: Context,
|
|
475
|
+
options: OpenAICodexResponsesOptions | undefined,
|
|
476
|
+
): Promise<RequestBody> {
|
|
477
|
+
const params: RequestBody = {
|
|
478
|
+
model: model.id,
|
|
479
|
+
input: [...convertMessages(model, context)],
|
|
480
|
+
stream: true,
|
|
481
|
+
prompt_cache_key: options?.sessionId,
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
if (options?.maxTokens) {
|
|
485
|
+
params.max_output_tokens = options.maxTokens;
|
|
486
|
+
}
|
|
487
|
+
if (options?.temperature !== undefined) {
|
|
488
|
+
params.temperature = options.temperature;
|
|
489
|
+
}
|
|
490
|
+
if (options?.topP !== undefined) {
|
|
491
|
+
params.top_p = options.topP;
|
|
492
|
+
}
|
|
493
|
+
if (options?.topK !== undefined) {
|
|
494
|
+
params.top_k = options.topK;
|
|
495
|
+
}
|
|
496
|
+
if (options?.minP !== undefined) {
|
|
497
|
+
params.min_p = options.minP;
|
|
498
|
+
}
|
|
499
|
+
if (options?.presencePenalty !== undefined) {
|
|
500
|
+
params.presence_penalty = options.presencePenalty;
|
|
501
|
+
}
|
|
502
|
+
if (options?.repetitionPenalty !== undefined) {
|
|
503
|
+
params.repetition_penalty = options.repetitionPenalty;
|
|
504
|
+
}
|
|
505
|
+
if (isSpecialServiceTier(options?.serviceTier)) {
|
|
506
|
+
params.service_tier = options.serviceTier;
|
|
507
|
+
}
|
|
508
|
+
if (context.tools && context.tools.length > 0) {
|
|
509
|
+
params.tools = convertTools(context.tools);
|
|
510
|
+
if (options?.toolChoice) {
|
|
511
|
+
const toolChoice = normalizeCodexToolChoice(options.toolChoice);
|
|
512
|
+
if (toolChoice) {
|
|
513
|
+
params.tool_choice = toolChoice;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const systemPrompt = buildCodexSystemPrompt({ userSystemPrompt: context.systemPrompt });
|
|
519
|
+
params.instructions = systemPrompt.instructions;
|
|
520
|
+
|
|
521
|
+
const codexOptions: CodexRequestOptions = {
|
|
522
|
+
reasoningEffort: options?.reasoning,
|
|
523
|
+
reasoningSummary: options?.reasoningSummary ?? "auto",
|
|
524
|
+
textVerbosity: options?.textVerbosity,
|
|
525
|
+
include: options?.include,
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
return transformRequestBody(params, model, codexOptions, systemPrompt);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function openInitialCodexEventStream(
|
|
532
|
+
model: Model<"openai-codex-responses">,
|
|
533
|
+
options: OpenAICodexResponsesOptions | undefined,
|
|
534
|
+
requestSetup: CodexRequestSetup,
|
|
535
|
+
requestContext: CodexRequestContext,
|
|
536
|
+
): Promise<{
|
|
537
|
+
eventStream: AsyncGenerator<Record<string, unknown>>;
|
|
538
|
+
requestBodyForState: RequestBody;
|
|
539
|
+
transport: CodexTransport;
|
|
540
|
+
}> {
|
|
541
|
+
const { transformedBody, websocketState } = requestContext;
|
|
542
|
+
if (websocketState && shouldUseCodexWebSocket(model, websocketState, options?.preferWebsockets)) {
|
|
543
|
+
const websocketRetryBudget = getCodexWebSocketRetryBudget();
|
|
544
|
+
let websocketRetries = 0;
|
|
545
|
+
while (true) {
|
|
546
|
+
try {
|
|
547
|
+
return await openCodexWebSocketTransport(
|
|
548
|
+
requestContext,
|
|
549
|
+
requestSetup,
|
|
550
|
+
options,
|
|
551
|
+
websocketState,
|
|
552
|
+
websocketRetries,
|
|
553
|
+
);
|
|
554
|
+
} catch (error) {
|
|
555
|
+
const websocketError = error instanceof Error ? error : new Error(String(error));
|
|
556
|
+
const isFatal = isCodexWebSocketFatalError(websocketError);
|
|
557
|
+
const activateFallback = isFatal || websocketRetries >= websocketRetryBudget;
|
|
558
|
+
recordCodexWebSocketFailure(websocketState, activateFallback);
|
|
559
|
+
logCodexDebug("codex websocket fallback", {
|
|
560
|
+
error: websocketError.message,
|
|
561
|
+
retry: websocketRetries,
|
|
562
|
+
retryBudget: websocketRetryBudget,
|
|
563
|
+
activated: activateFallback,
|
|
564
|
+
fatal: isFatal,
|
|
565
|
+
});
|
|
566
|
+
if (!activateFallback) {
|
|
567
|
+
websocketRetries += 1;
|
|
568
|
+
await abortableSleep(getCodexWebSocketRetryDelayMs(websocketRetries), requestSetup.requestSignal);
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return openCodexSseTransport(requestContext, requestSetup, options, websocketState, transformedBody);
|
|
576
|
+
}
|
|
577
|
+
async function openCodexWebSocketTransport(
|
|
578
|
+
requestContext: CodexRequestContext,
|
|
579
|
+
requestSetup: CodexRequestSetup,
|
|
580
|
+
options: OpenAICodexResponsesOptions | undefined,
|
|
581
|
+
websocketState: CodexWebSocketSessionState,
|
|
582
|
+
retry: number,
|
|
583
|
+
): Promise<{
|
|
584
|
+
eventStream: AsyncGenerator<Record<string, unknown>>;
|
|
585
|
+
requestBodyForState: RequestBody;
|
|
586
|
+
transport: CodexTransport;
|
|
587
|
+
}> {
|
|
588
|
+
const websocketRequest = buildCodexWebSocketRequest(requestContext.transformedBody, websocketState);
|
|
589
|
+
const websocketHeaders = createCodexHeaders(
|
|
590
|
+
requestContext.requestHeaders,
|
|
591
|
+
requestContext.accountId,
|
|
592
|
+
requestContext.apiKey,
|
|
593
|
+
options?.sessionId,
|
|
594
|
+
"websocket",
|
|
595
|
+
websocketState,
|
|
596
|
+
);
|
|
597
|
+
const requestBodyForState = structuredCloneJSON(requestContext.transformedBody);
|
|
598
|
+
logCodexDebug("codex websocket request", {
|
|
599
|
+
url: toWebSocketUrl(requestContext.url),
|
|
600
|
+
model: requestContext.transformedBody.model,
|
|
601
|
+
reasoningEffort: requestContext.transformedBody.reasoning?.effort ?? null,
|
|
602
|
+
headers: redactHeaders(websocketHeaders),
|
|
603
|
+
sentTurnStateHeader: websocketHeaders.has(X_CODEX_TURN_STATE_HEADER),
|
|
604
|
+
sentModelsEtagHeader: websocketHeaders.has(X_MODELS_ETAG_HEADER),
|
|
605
|
+
requestType: websocketRequest.type,
|
|
606
|
+
retry,
|
|
607
|
+
retryBudget: getCodexWebSocketRetryBudget(),
|
|
608
|
+
});
|
|
609
|
+
const eventStream = await openCodexWebSocketEventStream(
|
|
610
|
+
toWebSocketUrl(requestContext.url),
|
|
611
|
+
websocketHeaders,
|
|
612
|
+
websocketRequest,
|
|
613
|
+
websocketState,
|
|
614
|
+
requestSetup.requestSignal,
|
|
615
|
+
);
|
|
616
|
+
return { eventStream, requestBodyForState, transport: "websocket" };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function openCodexSseTransport(
|
|
620
|
+
requestContext: CodexRequestContext,
|
|
621
|
+
requestSetup: CodexRequestSetup,
|
|
622
|
+
options: OpenAICodexResponsesOptions | undefined,
|
|
623
|
+
state: CodexWebSocketSessionState | undefined,
|
|
624
|
+
body = requestContext.transformedBody,
|
|
625
|
+
): Promise<{
|
|
626
|
+
eventStream: AsyncGenerator<Record<string, unknown>>;
|
|
627
|
+
requestBodyForState: RequestBody;
|
|
628
|
+
transport: CodexTransport;
|
|
629
|
+
}> {
|
|
630
|
+
const eventStream = requestSetup.wrapCodexSseStream(
|
|
631
|
+
await openCodexSseEventStream(
|
|
632
|
+
requestContext.url,
|
|
633
|
+
requestContext.requestHeaders,
|
|
634
|
+
requestContext.accountId,
|
|
635
|
+
requestContext.apiKey,
|
|
636
|
+
options?.sessionId,
|
|
637
|
+
body,
|
|
638
|
+
state,
|
|
639
|
+
requestSetup.requestSignal,
|
|
640
|
+
),
|
|
641
|
+
);
|
|
642
|
+
return { eventStream, requestBodyForState: structuredCloneJSON(body), transport: "sse" };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async function reopenCodexWebSocketRuntimeStream(
|
|
646
|
+
context: CodexStreamProcessingContext,
|
|
647
|
+
runtime: CodexStreamRuntime,
|
|
648
|
+
state: CodexWebSocketSessionState,
|
|
649
|
+
): Promise<void> {
|
|
650
|
+
const next = await openCodexWebSocketTransport(
|
|
651
|
+
context.requestContext,
|
|
652
|
+
context.requestSetup,
|
|
653
|
+
context.options,
|
|
654
|
+
state,
|
|
655
|
+
runtime.websocketStreamRetries,
|
|
656
|
+
);
|
|
657
|
+
runtime.eventStream = next.eventStream;
|
|
658
|
+
runtime.requestBodyForState = next.requestBodyForState;
|
|
659
|
+
runtime.transport = next.transport;
|
|
660
|
+
state.lastTransport = next.transport;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async function reopenCodexSseRuntimeStream(
|
|
664
|
+
context: CodexStreamProcessingContext,
|
|
665
|
+
runtime: CodexStreamRuntime,
|
|
666
|
+
state: CodexWebSocketSessionState | undefined,
|
|
667
|
+
): Promise<void> {
|
|
668
|
+
const next = await openCodexSseTransport(context.requestContext, context.requestSetup, context.options, state);
|
|
669
|
+
runtime.eventStream = next.eventStream;
|
|
670
|
+
runtime.requestBodyForState = next.requestBodyForState;
|
|
671
|
+
runtime.transport = next.transport;
|
|
672
|
+
if (state) {
|
|
673
|
+
state.lastTransport = next.transport;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function createCodexStreamRuntime(initial: {
|
|
678
|
+
eventStream: AsyncGenerator<Record<string, unknown>>;
|
|
679
|
+
requestBodyForState: RequestBody;
|
|
680
|
+
transport: CodexTransport;
|
|
681
|
+
websocketState?: CodexWebSocketSessionState;
|
|
682
|
+
}): CodexStreamRuntime {
|
|
683
|
+
return {
|
|
684
|
+
eventStream: initial.eventStream,
|
|
685
|
+
requestBodyForState: initial.requestBodyForState,
|
|
686
|
+
transport: initial.transport,
|
|
687
|
+
websocketState: initial.websocketState,
|
|
688
|
+
currentItem: null,
|
|
689
|
+
currentBlock: null,
|
|
690
|
+
nativeOutputItems: [],
|
|
691
|
+
websocketStreamRetries: 0,
|
|
692
|
+
providerRetryAttempt: 0,
|
|
693
|
+
sawTerminalEvent: false,
|
|
694
|
+
canSafelyReplayWebsocketOverSse: true,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async function processCodexResponseStream(
|
|
699
|
+
context: CodexStreamProcessingContext,
|
|
700
|
+
runtime: CodexStreamRuntime,
|
|
701
|
+
): Promise<CodexStreamCompletion> {
|
|
702
|
+
const { output, stream } = context;
|
|
703
|
+
stream.push({ type: "start", partial: output });
|
|
704
|
+
|
|
705
|
+
while (true) {
|
|
706
|
+
try {
|
|
707
|
+
let firstTokenTime = context.firstTokenTime;
|
|
708
|
+
for await (const rawEvent of runtime.eventStream) {
|
|
709
|
+
firstTokenTime = handleCodexStreamEvent({
|
|
710
|
+
...context,
|
|
711
|
+
runtime,
|
|
712
|
+
rawEvent,
|
|
713
|
+
firstTokenTime,
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
return { firstTokenTime };
|
|
717
|
+
} catch (error) {
|
|
718
|
+
const recovered = await recoverCodexStreamError(context, runtime, error);
|
|
719
|
+
if (!recovered) {
|
|
720
|
+
throw error;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function handleCodexStreamEvent(args: {
|
|
727
|
+
model: Model<"openai-codex-responses">;
|
|
728
|
+
output: AssistantMessage;
|
|
729
|
+
stream: AssistantMessageEventStream;
|
|
730
|
+
runtime: CodexStreamRuntime;
|
|
731
|
+
rawEvent: Record<string, unknown>;
|
|
732
|
+
firstTokenTime?: number;
|
|
733
|
+
}): number | undefined {
|
|
734
|
+
const { model, output, stream, runtime, rawEvent } = args;
|
|
735
|
+
const eventType = typeof rawEvent.type === "string" ? rawEvent.type : "";
|
|
736
|
+
if (!eventType) return args.firstTokenTime;
|
|
737
|
+
|
|
738
|
+
const blocks = output.content;
|
|
739
|
+
const blockIndex = () => blocks.length - 1;
|
|
740
|
+
let firstTokenTime = args.firstTokenTime;
|
|
741
|
+
|
|
742
|
+
if (eventType === "response.output_item.added") {
|
|
743
|
+
if (!firstTokenTime) firstTokenTime = Date.now();
|
|
744
|
+
const item = rawEvent.item as CodexEventItem;
|
|
745
|
+
runtime.currentItem = item;
|
|
746
|
+
runtime.currentBlock = createOutputBlockForItem(item);
|
|
747
|
+
if (!runtime.currentBlock) return firstTokenTime;
|
|
748
|
+
output.content.push(runtime.currentBlock);
|
|
749
|
+
stream.push({
|
|
750
|
+
type: getOutputBlockStartEventType(runtime.currentBlock),
|
|
751
|
+
contentIndex: blockIndex(),
|
|
752
|
+
partial: output,
|
|
753
|
+
});
|
|
754
|
+
return firstTokenTime;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (eventType === "response.reasoning_summary_part.added") {
|
|
758
|
+
handleReasoningSummaryPartAdded(runtime.currentItem, rawEvent);
|
|
759
|
+
return firstTokenTime;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (eventType === "response.reasoning_summary_text.delta") {
|
|
763
|
+
handleReasoningSummaryTextDelta(runtime.currentItem, runtime.currentBlock, rawEvent, stream, output, blockIndex);
|
|
764
|
+
return firstTokenTime;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (eventType === "response.reasoning_summary_part.done") {
|
|
768
|
+
handleReasoningSummaryPartDone(runtime.currentItem, runtime.currentBlock, stream, output, blockIndex);
|
|
769
|
+
return firstTokenTime;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (eventType === "response.content_part.added") {
|
|
773
|
+
handleContentPartAdded(runtime.currentItem, rawEvent);
|
|
774
|
+
return firstTokenTime;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (eventType === "response.output_text.delta") {
|
|
778
|
+
handleMessageTextDelta(
|
|
779
|
+
runtime.currentItem,
|
|
780
|
+
runtime.currentBlock,
|
|
781
|
+
rawEvent,
|
|
782
|
+
stream,
|
|
783
|
+
output,
|
|
784
|
+
blockIndex,
|
|
785
|
+
"output_text",
|
|
786
|
+
);
|
|
787
|
+
return firstTokenTime;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (eventType === "response.refusal.delta") {
|
|
791
|
+
handleMessageTextDelta(
|
|
792
|
+
runtime.currentItem,
|
|
793
|
+
runtime.currentBlock,
|
|
794
|
+
rawEvent,
|
|
795
|
+
stream,
|
|
796
|
+
output,
|
|
797
|
+
blockIndex,
|
|
798
|
+
"refusal",
|
|
799
|
+
);
|
|
800
|
+
return firstTokenTime;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (eventType === "response.function_call_arguments.delta") {
|
|
804
|
+
handleToolCallArgumentsDelta(runtime.currentItem, runtime.currentBlock, rawEvent, stream, output, blockIndex);
|
|
805
|
+
return firstTokenTime;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (eventType === "response.function_call_arguments.done") {
|
|
809
|
+
handleToolCallArgumentsDone(runtime.currentItem, runtime.currentBlock, rawEvent);
|
|
810
|
+
return firstTokenTime;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (eventType === "response.output_item.done") {
|
|
814
|
+
handleOutputItemDone(model, output, stream, runtime, rawEvent, blockIndex);
|
|
815
|
+
return firstTokenTime;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (eventType === "response.created") {
|
|
819
|
+
return handleResponseCreated(runtime, rawEvent);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (eventType === "response.completed" || eventType === "response.done" || eventType === "response.incomplete") {
|
|
823
|
+
handleResponseCompleted(model, output, runtime, rawEvent);
|
|
824
|
+
return firstTokenTime;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (eventType === "error" || eventType === "response.failed") {
|
|
828
|
+
throw createCodexProviderStreamError(rawEvent);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return firstTokenTime;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function createOutputBlockForItem(item: CodexEventItem): CodexOutputBlock | null {
|
|
835
|
+
if (item.type === "reasoning") {
|
|
836
|
+
return { type: "thinking", thinking: "" };
|
|
837
|
+
}
|
|
838
|
+
if (item.type === "message") {
|
|
839
|
+
return { type: "text", text: "" };
|
|
840
|
+
}
|
|
841
|
+
if (item.type === "function_call") {
|
|
842
|
+
return {
|
|
843
|
+
type: "toolCall",
|
|
844
|
+
id: `${item.call_id}|${item.id}`,
|
|
845
|
+
name: item.name,
|
|
846
|
+
arguments: {},
|
|
847
|
+
partialJson: item.arguments || "",
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function getOutputBlockStartEventType(block: CodexOutputBlock): "thinking_start" | "text_start" | "toolcall_start" {
|
|
854
|
+
if (block.type === "thinking") return "thinking_start";
|
|
855
|
+
if (block.type === "text") return "text_start";
|
|
856
|
+
return "toolcall_start";
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function handleReasoningSummaryPartAdded(currentItem: CodexEventItem | null, rawEvent: Record<string, unknown>): void {
|
|
860
|
+
if (currentItem?.type !== "reasoning") return;
|
|
861
|
+
currentItem.summary = currentItem.summary || [];
|
|
862
|
+
currentItem.summary.push((rawEvent as { part: ResponseReasoningItem["summary"][number] }).part);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function handleReasoningSummaryTextDelta(
|
|
866
|
+
currentItem: CodexEventItem | null,
|
|
867
|
+
currentBlock: CodexOutputBlock | null,
|
|
868
|
+
rawEvent: Record<string, unknown>,
|
|
869
|
+
stream: AssistantMessageEventStream,
|
|
870
|
+
output: AssistantMessage,
|
|
871
|
+
blockIndex: () => number,
|
|
872
|
+
): void {
|
|
873
|
+
if (currentItem?.type !== "reasoning" || currentBlock?.type !== "thinking") return;
|
|
874
|
+
currentItem.summary = currentItem.summary || [];
|
|
875
|
+
const lastPart = currentItem.summary[currentItem.summary.length - 1];
|
|
876
|
+
if (!lastPart) return;
|
|
877
|
+
const delta = (rawEvent as { delta?: string }).delta || "";
|
|
878
|
+
currentBlock.thinking += delta;
|
|
879
|
+
lastPart.text += delta;
|
|
880
|
+
stream.push({ type: "thinking_delta", contentIndex: blockIndex(), delta, partial: output });
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function handleReasoningSummaryPartDone(
|
|
884
|
+
currentItem: CodexEventItem | null,
|
|
885
|
+
currentBlock: CodexOutputBlock | null,
|
|
886
|
+
stream: AssistantMessageEventStream,
|
|
887
|
+
output: AssistantMessage,
|
|
888
|
+
blockIndex: () => number,
|
|
889
|
+
): void {
|
|
890
|
+
if (currentItem?.type !== "reasoning" || currentBlock?.type !== "thinking") return;
|
|
891
|
+
currentItem.summary = currentItem.summary || [];
|
|
892
|
+
const lastPart = currentItem.summary[currentItem.summary.length - 1];
|
|
893
|
+
if (!lastPart) return;
|
|
894
|
+
currentBlock.thinking += "\n\n";
|
|
895
|
+
lastPart.text += "\n\n";
|
|
896
|
+
stream.push({ type: "thinking_delta", contentIndex: blockIndex(), delta: "\n\n", partial: output });
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function handleContentPartAdded(currentItem: CodexEventItem | null, rawEvent: Record<string, unknown>): void {
|
|
900
|
+
if (currentItem?.type !== "message") return;
|
|
901
|
+
currentItem.content = currentItem.content || [];
|
|
902
|
+
const part = (rawEvent as { part?: ResponseOutputMessage["content"][number] }).part;
|
|
903
|
+
if (part && (part.type === "output_text" || part.type === "refusal")) {
|
|
904
|
+
currentItem.content.push(part);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function handleMessageTextDelta(
|
|
909
|
+
currentItem: CodexEventItem | null,
|
|
910
|
+
currentBlock: CodexOutputBlock | null,
|
|
911
|
+
rawEvent: Record<string, unknown>,
|
|
912
|
+
stream: AssistantMessageEventStream,
|
|
913
|
+
output: AssistantMessage,
|
|
914
|
+
blockIndex: () => number,
|
|
915
|
+
partType: "output_text" | "refusal",
|
|
916
|
+
): void {
|
|
917
|
+
if (currentItem?.type !== "message" || currentBlock?.type !== "text") return;
|
|
918
|
+
if (!currentItem.content || currentItem.content.length === 0) return;
|
|
919
|
+
const lastPart = currentItem.content[currentItem.content.length - 1];
|
|
920
|
+
if (!lastPart || lastPart.type !== partType) return;
|
|
921
|
+
const delta = (rawEvent as { delta?: string }).delta || "";
|
|
922
|
+
currentBlock.text += delta;
|
|
923
|
+
if (lastPart.type === "output_text") {
|
|
924
|
+
lastPart.text += delta;
|
|
925
|
+
} else {
|
|
926
|
+
lastPart.refusal += delta;
|
|
927
|
+
}
|
|
928
|
+
stream.push({ type: "text_delta", contentIndex: blockIndex(), delta, partial: output });
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function handleToolCallArgumentsDelta(
|
|
932
|
+
currentItem: CodexEventItem | null,
|
|
933
|
+
currentBlock: CodexOutputBlock | null,
|
|
934
|
+
rawEvent: Record<string, unknown>,
|
|
935
|
+
stream: AssistantMessageEventStream,
|
|
936
|
+
output: AssistantMessage,
|
|
937
|
+
blockIndex: () => number,
|
|
938
|
+
): void {
|
|
939
|
+
if (currentItem?.type !== "function_call" || currentBlock?.type !== "toolCall") return;
|
|
940
|
+
const delta = (rawEvent as { delta?: string }).delta || "";
|
|
941
|
+
currentBlock.partialJson += delta;
|
|
942
|
+
currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
|
|
943
|
+
stream.push({ type: "toolcall_delta", contentIndex: blockIndex(), delta, partial: output });
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function handleToolCallArgumentsDone(
|
|
947
|
+
currentItem: CodexEventItem | null,
|
|
948
|
+
currentBlock: CodexOutputBlock | null,
|
|
949
|
+
rawEvent: Record<string, unknown>,
|
|
950
|
+
): void {
|
|
951
|
+
if (currentItem?.type !== "function_call" || currentBlock?.type !== "toolCall") return;
|
|
952
|
+
const args = (rawEvent as { arguments?: string }).arguments;
|
|
953
|
+
if (typeof args === "string") {
|
|
954
|
+
currentBlock.partialJson = args;
|
|
955
|
+
currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function handleOutputItemDone(
|
|
960
|
+
model: Model<"openai-codex-responses">,
|
|
961
|
+
output: AssistantMessage,
|
|
962
|
+
stream: AssistantMessageEventStream,
|
|
963
|
+
runtime: CodexStreamRuntime,
|
|
964
|
+
rawEvent: Record<string, unknown>,
|
|
965
|
+
blockIndex: () => number,
|
|
966
|
+
): void {
|
|
967
|
+
const item = structuredCloneJSON(rawEvent.item) as CodexEventItem;
|
|
968
|
+
runtime.nativeOutputItems.push(item as unknown as Record<string, unknown>);
|
|
969
|
+
|
|
970
|
+
if (item.type === "reasoning" && runtime.currentBlock?.type === "thinking") {
|
|
971
|
+
runtime.currentBlock.thinking = item.summary?.map(summary => summary.text).join("\n\n") || "";
|
|
972
|
+
runtime.currentBlock.thinkingSignature = JSON.stringify(item);
|
|
973
|
+
stream.push({
|
|
974
|
+
type: "thinking_end",
|
|
975
|
+
contentIndex: blockIndex(),
|
|
976
|
+
content: runtime.currentBlock.thinking,
|
|
977
|
+
partial: output,
|
|
978
|
+
});
|
|
979
|
+
runtime.currentBlock = null;
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (item.type === "message" && runtime.currentBlock?.type === "text") {
|
|
984
|
+
runtime.currentBlock.text = item.content
|
|
985
|
+
.map(content => (content.type === "output_text" ? content.text : content.refusal))
|
|
986
|
+
.join("");
|
|
987
|
+
const phase = item.phase === "commentary" || item.phase === "final_answer" ? item.phase : undefined;
|
|
988
|
+
runtime.currentBlock.textSignature = encodeTextSignatureV1(item.id, phase);
|
|
989
|
+
stream.push({
|
|
990
|
+
type: "text_end",
|
|
991
|
+
contentIndex: blockIndex(),
|
|
992
|
+
content: runtime.currentBlock.text,
|
|
993
|
+
partial: output,
|
|
994
|
+
});
|
|
995
|
+
runtime.currentBlock = null;
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (item.type === "function_call") {
|
|
1000
|
+
const toolCall: ToolCall = {
|
|
1001
|
+
type: "toolCall",
|
|
1002
|
+
id: `${item.call_id}|${item.id}`,
|
|
1003
|
+
name: item.name,
|
|
1004
|
+
arguments: parseStreamingJson(item.arguments || "{}"),
|
|
1005
|
+
};
|
|
1006
|
+
runtime.canSafelyReplayWebsocketOverSse = false;
|
|
1007
|
+
stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
void model;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function handleResponseCreated(runtime: CodexStreamRuntime, rawEvent: Record<string, unknown>): number | undefined {
|
|
1015
|
+
const response = (rawEvent as { response?: { id?: string } }).response;
|
|
1016
|
+
const state = runtime.websocketState;
|
|
1017
|
+
if (runtime.transport === "websocket" && state && typeof response?.id === "string" && response.id.length > 0) {
|
|
1018
|
+
state.lastResponseId = response.id;
|
|
1019
|
+
}
|
|
1020
|
+
return undefined;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function handleResponseCompleted(
|
|
1024
|
+
model: Model<"openai-codex-responses">,
|
|
1025
|
+
output: AssistantMessage,
|
|
1026
|
+
runtime: CodexStreamRuntime,
|
|
1027
|
+
rawEvent: Record<string, unknown>,
|
|
1028
|
+
): void {
|
|
1029
|
+
runtime.sawTerminalEvent = true;
|
|
1030
|
+
const response = (
|
|
1031
|
+
rawEvent as {
|
|
1032
|
+
response?: {
|
|
1033
|
+
id?: string;
|
|
1034
|
+
usage?: {
|
|
1035
|
+
input_tokens?: number;
|
|
1036
|
+
output_tokens?: number;
|
|
1037
|
+
total_tokens?: number;
|
|
1038
|
+
input_tokens_details?: { cached_tokens?: number };
|
|
1039
|
+
};
|
|
1040
|
+
status?: string;
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
).response;
|
|
1044
|
+
|
|
1045
|
+
if (response?.usage) {
|
|
1046
|
+
const cachedTokens = response.usage.input_tokens_details?.cached_tokens || 0;
|
|
1047
|
+
output.usage = {
|
|
1048
|
+
input: (response.usage.input_tokens || 0) - cachedTokens,
|
|
1049
|
+
output: response.usage.output_tokens || 0,
|
|
1050
|
+
cacheRead: cachedTokens,
|
|
1051
|
+
cacheWrite: 0,
|
|
1052
|
+
totalTokens: response.usage.total_tokens || 0,
|
|
1053
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
if (typeof response?.id === "string" && response.id.length > 0) {
|
|
1057
|
+
output.responseId = response.id;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
const state = runtime.websocketState;
|
|
1061
|
+
if (runtime.transport === "websocket" && state) {
|
|
1062
|
+
state.lastRequest = structuredCloneJSON(runtime.requestBodyForState);
|
|
1063
|
+
if (typeof response?.id === "string" && response.id.length > 0) {
|
|
1064
|
+
state.lastResponseId = response.id;
|
|
1065
|
+
}
|
|
1066
|
+
state.canAppend = rawEvent.type === "response.done";
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
calculateCost(model, output.usage);
|
|
1070
|
+
output.stopReason = mapOpenAIResponsesStopReason(response?.status as OpenAI.Responses.ResponseStatus | undefined);
|
|
1071
|
+
if (output.content.some(block => block.type === "toolCall") && output.stopReason === "stop") {
|
|
1072
|
+
output.stopReason = "toolUse";
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
async function recoverCodexStreamError(
|
|
1077
|
+
context: CodexStreamProcessingContext,
|
|
1078
|
+
runtime: CodexStreamRuntime,
|
|
1079
|
+
error: unknown,
|
|
1080
|
+
): Promise<boolean> {
|
|
1081
|
+
if (await tryReconnectCodexWebSocketOnConnectionLimit(context, runtime, error)) {
|
|
1082
|
+
return true;
|
|
1083
|
+
}
|
|
1084
|
+
if (await tryReplayWebsocketFailureOverSse(context, runtime, error)) {
|
|
1085
|
+
return true;
|
|
1086
|
+
}
|
|
1087
|
+
if (await tryRetryCodexProviderError(context, runtime, error)) {
|
|
1088
|
+
return true;
|
|
1089
|
+
}
|
|
1090
|
+
return false;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Handles `websocket_connection_limit_reached` errors by closing the stale connection
|
|
1095
|
+
* and opening a fresh websocket. If content has already been emitted to the caller,
|
|
1096
|
+
* falls back to SSE replay (same as other WS failures) since we cannot safely
|
|
1097
|
+
* continue a partial response on a new connection.
|
|
1098
|
+
*/
|
|
1099
|
+
async function tryReconnectCodexWebSocketOnConnectionLimit(
|
|
1100
|
+
context: CodexStreamProcessingContext,
|
|
1101
|
+
runtime: CodexStreamRuntime,
|
|
1102
|
+
error: unknown,
|
|
1103
|
+
): Promise<boolean> {
|
|
1104
|
+
if (!(error instanceof CodexProviderStreamError) || error.code !== "websocket_connection_limit_reached") {
|
|
1105
|
+
return false;
|
|
1106
|
+
}
|
|
1107
|
+
const websocketState = context.requestContext.websocketState;
|
|
1108
|
+
if (!websocketState || runtime.transport !== "websocket" || context.options?.signal?.aborted) {
|
|
1109
|
+
return false;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Close the stale connection so getOrCreateCodexWebSocketConnection creates a fresh one.
|
|
1113
|
+
websocketState.connection?.close("connection_limit");
|
|
1114
|
+
websocketState.connection = undefined;
|
|
1115
|
+
resetCodexWebSocketAppendState(websocketState);
|
|
1116
|
+
|
|
1117
|
+
logCodexDebug("codex websocket connection limit reached, reconnecting", {
|
|
1118
|
+
hadContent: context.output.content.length > 0,
|
|
1119
|
+
retry: runtime.websocketStreamRetries,
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
if (context.output.content.length > 0) {
|
|
1123
|
+
// Content already emitted to the caller — cannot safely continue on a new WS.
|
|
1124
|
+
// Reset and replay the full request over SSE.
|
|
1125
|
+
runtime.canSafelyReplayWebsocketOverSse = true;
|
|
1126
|
+
runtime.currentItem = null;
|
|
1127
|
+
runtime.currentBlock = null;
|
|
1128
|
+
runtime.nativeOutputItems.length = 0;
|
|
1129
|
+
resetOutputState(context.output);
|
|
1130
|
+
context.firstTokenTime = undefined;
|
|
1131
|
+
recordCodexWebSocketFailure(websocketState, true);
|
|
1132
|
+
await reopenCodexSseRuntimeStream(context, runtime, websocketState);
|
|
1133
|
+
return true;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// No content emitted yet — reconnect over websocket.
|
|
1137
|
+
runtime.websocketStreamRetries += 1;
|
|
1138
|
+
await reopenCodexWebSocketRuntimeStream(context, runtime, websocketState);
|
|
1139
|
+
return true;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
async function tryReplayWebsocketFailureOverSse(
|
|
1143
|
+
context: CodexStreamProcessingContext,
|
|
1144
|
+
runtime: CodexStreamRuntime,
|
|
1145
|
+
error: unknown,
|
|
1146
|
+
): Promise<boolean> {
|
|
1147
|
+
const websocketState = context.requestContext.websocketState;
|
|
1148
|
+
const canReplay =
|
|
1149
|
+
runtime.transport === "websocket" &&
|
|
1150
|
+
websocketState &&
|
|
1151
|
+
isCodexWebSocketRetryableStreamError(error) &&
|
|
1152
|
+
runtime.canSafelyReplayWebsocketOverSse &&
|
|
1153
|
+
!runtime.sawTerminalEvent &&
|
|
1154
|
+
!context.options?.signal?.aborted;
|
|
1155
|
+
if (!canReplay) return false;
|
|
1156
|
+
|
|
1157
|
+
const state = websocketState;
|
|
1158
|
+
const streamError = error instanceof Error ? error : new Error(String(error));
|
|
1159
|
+
const replayingBufferedOutputOverSse = context.output.content.length > 0;
|
|
1160
|
+
const isFatal = isCodexWebSocketFatalError(streamError);
|
|
1161
|
+
const activateFallback =
|
|
1162
|
+
replayingBufferedOutputOverSse || isFatal || runtime.websocketStreamRetries >= getCodexWebSocketRetryBudget();
|
|
1163
|
+
recordCodexWebSocketFailure(state, activateFallback);
|
|
1164
|
+
logCodexDebug("codex websocket stream fallback", {
|
|
1165
|
+
error: streamError.message,
|
|
1166
|
+
retry: runtime.websocketStreamRetries,
|
|
1167
|
+
retryBudget: getCodexWebSocketRetryBudget(),
|
|
1168
|
+
activated: activateFallback,
|
|
1169
|
+
fatal: isFatal,
|
|
1170
|
+
replayedBufferedOutput: replayingBufferedOutputOverSse,
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
if (!activateFallback) {
|
|
1174
|
+
runtime.websocketStreamRetries += 1;
|
|
1175
|
+
await abortableSleep(
|
|
1176
|
+
getCodexWebSocketRetryDelayMs(runtime.websocketStreamRetries),
|
|
1177
|
+
context.requestSetup.requestSignal,
|
|
1178
|
+
);
|
|
1179
|
+
await reopenCodexWebSocketRuntimeStream(context, runtime, state);
|
|
1180
|
+
return true;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
if (replayingBufferedOutputOverSse) {
|
|
1184
|
+
runtime.canSafelyReplayWebsocketOverSse = true;
|
|
1185
|
+
runtime.currentItem = null;
|
|
1186
|
+
runtime.currentBlock = null;
|
|
1187
|
+
runtime.nativeOutputItems.length = 0;
|
|
1188
|
+
resetOutputState(context.output);
|
|
1189
|
+
context.firstTokenTime = undefined;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
await reopenCodexSseRuntimeStream(context, runtime, state);
|
|
1193
|
+
return true;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
async function tryRetryCodexProviderError(
|
|
1197
|
+
context: CodexStreamProcessingContext,
|
|
1198
|
+
runtime: CodexStreamRuntime,
|
|
1199
|
+
error: unknown,
|
|
1200
|
+
): Promise<boolean> {
|
|
1201
|
+
if (
|
|
1202
|
+
!isRetryableCodexProviderError(error) ||
|
|
1203
|
+
context.output.content.length > 0 ||
|
|
1204
|
+
runtime.providerRetryAttempt >= CODEX_MAX_RETRIES ||
|
|
1205
|
+
context.options?.signal?.aborted
|
|
1206
|
+
) {
|
|
1207
|
+
return false;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
runtime.providerRetryAttempt += 1;
|
|
1211
|
+
const websocketState = context.requestContext.websocketState;
|
|
1212
|
+
if (runtime.transport === "websocket" && websocketState) {
|
|
1213
|
+
resetCodexWebSocketAppendState(websocketState);
|
|
1214
|
+
resetCodexSessionMetadata(websocketState);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
logCodexDebug("retrying codex provider stream error", {
|
|
1218
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1219
|
+
retry: runtime.providerRetryAttempt,
|
|
1220
|
+
retryBudget: CODEX_MAX_RETRIES,
|
|
1221
|
+
transport: runtime.transport,
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
runtime.currentItem = null;
|
|
1225
|
+
runtime.currentBlock = null;
|
|
1226
|
+
runtime.sawTerminalEvent = false;
|
|
1227
|
+
resetOutputState(context.output);
|
|
1228
|
+
context.firstTokenTime = undefined;
|
|
1229
|
+
await abortableSleep(CODEX_RETRY_DELAY_MS * runtime.providerRetryAttempt, context.requestSetup.requestSignal);
|
|
1230
|
+
|
|
1231
|
+
if (runtime.transport === "websocket" && websocketState) {
|
|
1232
|
+
await reopenCodexWebSocketRuntimeStream(context, runtime, websocketState);
|
|
1233
|
+
return true;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
await reopenCodexSseRuntimeStream(context, runtime, websocketState);
|
|
1237
|
+
return true;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function finalizeCodexResponse(
|
|
1241
|
+
context: CodexStreamProcessingContext,
|
|
1242
|
+
runtime: CodexStreamRuntime,
|
|
1243
|
+
completion: CodexStreamCompletion,
|
|
1244
|
+
): AssistantMessage {
|
|
1245
|
+
const { output } = context;
|
|
1246
|
+
if (context.options?.signal?.aborted) {
|
|
1247
|
+
throw new Error("Request was aborted");
|
|
1248
|
+
}
|
|
1249
|
+
if (!runtime.sawTerminalEvent) {
|
|
1250
|
+
if (runtime.transport === "websocket" && context.requestContext.websocketState) {
|
|
1251
|
+
resetCodexWebSocketAppendState(context.requestContext.websocketState);
|
|
1252
|
+
resetCodexSessionMetadata(context.requestContext.websocketState);
|
|
1253
|
+
}
|
|
1254
|
+
logCodexDebug("codex stream ended unexpectedly", {
|
|
1255
|
+
transport: runtime.transport,
|
|
1256
|
+
terminalEventSeen: runtime.sawTerminalEvent,
|
|
1257
|
+
unexpectedStreamEnd: true,
|
|
1258
|
+
sentTurnStateHeader: Boolean(context.requestContext.websocketState?.turnState),
|
|
1259
|
+
sentModelsEtagHeader: Boolean(context.requestContext.websocketState?.modelsEtag),
|
|
1260
|
+
});
|
|
1261
|
+
throw new Error("Codex stream ended before terminal completion event");
|
|
1262
|
+
}
|
|
1263
|
+
if (output.stopReason === "aborted" || output.stopReason === "error") {
|
|
1264
|
+
throw new Error("Codex response failed");
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
output.providerPayload = createOpenAIResponsesHistoryPayload(context.model.provider, runtime.nativeOutputItems);
|
|
1268
|
+
output.duration = Date.now() - context.startTime;
|
|
1269
|
+
if (completion.firstTokenTime) {
|
|
1270
|
+
output.ttft = completion.firstTokenTime - context.startTime;
|
|
1271
|
+
}
|
|
1272
|
+
return output;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
async function handleCodexStreamFailure(
|
|
1276
|
+
context: CodexStreamProcessingContext,
|
|
1277
|
+
error: unknown,
|
|
1278
|
+
): Promise<AssistantMessage> {
|
|
1279
|
+
const { output } = context;
|
|
1280
|
+
removeTransientBlockIndices(output);
|
|
1281
|
+
if (context.requestContext.websocketState) {
|
|
1282
|
+
resetCodexWebSocketAppendState(context.requestContext.websocketState);
|
|
1283
|
+
resetCodexSessionMetadata(context.requestContext.websocketState);
|
|
1284
|
+
}
|
|
1285
|
+
output.stopReason = context.options?.signal?.aborted ? "aborted" : "error";
|
|
1286
|
+
output.errorMessage = await finalizeErrorMessage(error, context.requestContext.rawRequestDump);
|
|
1287
|
+
output.duration = Date.now() - context.startTime;
|
|
1288
|
+
if (context.firstTokenTime) {
|
|
1289
|
+
output.ttft = context.firstTokenTime - context.startTime;
|
|
1290
|
+
}
|
|
1291
|
+
return output;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"> = (
|
|
1295
|
+
model: Model<"openai-codex-responses">,
|
|
1296
|
+
context: Context,
|
|
1297
|
+
options?: OpenAICodexResponsesOptions,
|
|
1298
|
+
): AssistantMessageEventStream => {
|
|
1299
|
+
const stream = new AssistantMessageEventStream();
|
|
1300
|
+
|
|
1301
|
+
(async () => {
|
|
1302
|
+
const startTime = Date.now();
|
|
1303
|
+
const output = createAssistantOutput(model);
|
|
1304
|
+
const requestSetup = createRequestSetup(options);
|
|
1305
|
+
let processingContext: CodexStreamProcessingContext | undefined;
|
|
1306
|
+
|
|
1307
|
+
try {
|
|
1308
|
+
const requestContext = await buildCodexRequestContext(model, context, options, output);
|
|
1309
|
+
const initialTransport = await openInitialCodexEventStream(model, options, requestSetup, requestContext);
|
|
1310
|
+
const runtime = createCodexStreamRuntime({
|
|
1311
|
+
...initialTransport,
|
|
1312
|
+
websocketState: requestContext.websocketState,
|
|
1313
|
+
});
|
|
1314
|
+
if (requestContext.websocketState) {
|
|
1315
|
+
requestContext.websocketState.lastTransport = initialTransport.transport;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
processingContext = {
|
|
1319
|
+
model,
|
|
1320
|
+
output,
|
|
1321
|
+
stream,
|
|
1322
|
+
options,
|
|
1323
|
+
requestSetup,
|
|
1324
|
+
requestContext,
|
|
1325
|
+
startTime,
|
|
1326
|
+
};
|
|
1327
|
+
|
|
1328
|
+
const completion = await processCodexResponseStream(processingContext, runtime);
|
|
1329
|
+
processingContext.firstTokenTime = completion.firstTokenTime;
|
|
1330
|
+
const message = finalizeCodexResponse(processingContext, runtime, completion);
|
|
1331
|
+
stream.push({ type: "done", reason: message.stopReason as "stop" | "length" | "toolUse", message });
|
|
1332
|
+
stream.end();
|
|
1333
|
+
} catch (error) {
|
|
1334
|
+
const failureContext =
|
|
1335
|
+
processingContext ??
|
|
1336
|
+
({
|
|
1337
|
+
model,
|
|
1338
|
+
output,
|
|
1339
|
+
stream,
|
|
1340
|
+
options,
|
|
1341
|
+
requestSetup,
|
|
1342
|
+
requestContext: {
|
|
1343
|
+
apiKey: "",
|
|
1344
|
+
accountId: "",
|
|
1345
|
+
baseUrl: model.baseUrl || CODEX_BASE_URL,
|
|
1346
|
+
url: "",
|
|
1347
|
+
requestHeaders: {},
|
|
1348
|
+
transformedBody: { model: model.id },
|
|
1349
|
+
rawRequestDump: {
|
|
1350
|
+
provider: model.provider,
|
|
1351
|
+
api: output.api,
|
|
1352
|
+
model: model.id,
|
|
1353
|
+
method: "POST",
|
|
1354
|
+
url: "",
|
|
1355
|
+
body: { model: model.id },
|
|
1356
|
+
},
|
|
1357
|
+
},
|
|
1358
|
+
startTime,
|
|
1359
|
+
} satisfies CodexStreamProcessingContext);
|
|
1360
|
+
const failure = await handleCodexStreamFailure(failureContext, error);
|
|
1361
|
+
stream.push({ type: "error", reason: failure.stopReason as "error" | "aborted", error: failure });
|
|
1362
|
+
stream.end();
|
|
1363
|
+
}
|
|
1364
|
+
})();
|
|
1365
|
+
|
|
1366
|
+
return stream;
|
|
1367
|
+
};
|
|
1368
|
+
|
|
1369
|
+
export async function prewarmOpenAICodexResponses(
|
|
1370
|
+
model: Model<"openai-codex-responses">,
|
|
1371
|
+
options?: Pick<
|
|
1372
|
+
OpenAICodexResponsesOptions,
|
|
1373
|
+
"apiKey" | "headers" | "sessionId" | "signal" | "preferWebsockets" | "providerSessionState"
|
|
1374
|
+
>,
|
|
1375
|
+
): Promise<void> {
|
|
1376
|
+
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
1377
|
+
if (!apiKey) return;
|
|
1378
|
+
const accountId = getAccountId(apiKey);
|
|
1379
|
+
const baseUrl = model.baseUrl || CODEX_BASE_URL;
|
|
1380
|
+
const baseWithSlash = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
1381
|
+
const url = rewriteUrlForCodex(new URL(URL_PATHS.RESPONSES.slice(1), baseWithSlash).toString());
|
|
1382
|
+
const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
|
|
1383
|
+
const sessionKey = getCodexWebSocketSessionKey(options?.sessionId, model, accountId, baseUrl);
|
|
1384
|
+
const publicSessionKey = getCodexPublicSessionKey(options?.sessionId, model, baseUrl);
|
|
1385
|
+
if (publicSessionKey && sessionKey) {
|
|
1386
|
+
providerSessionState?.webSocketPublicToPrivate.set(publicSessionKey, sessionKey);
|
|
1387
|
+
}
|
|
1388
|
+
if (!sessionKey || !providerSessionState) return;
|
|
1389
|
+
const state = getCodexWebSocketSessionState(sessionKey, providerSessionState);
|
|
1390
|
+
if (!shouldUseCodexWebSocket(model, state, options?.preferWebsockets)) return;
|
|
1391
|
+
logger.time("prewarmCodex:createHeaders");
|
|
1392
|
+
const headers = createCodexHeaders(
|
|
1393
|
+
{ ...(model.headers ?? {}), ...(options?.headers ?? {}) },
|
|
1394
|
+
accountId,
|
|
1395
|
+
apiKey,
|
|
1396
|
+
options?.sessionId,
|
|
1397
|
+
"websocket",
|
|
1398
|
+
state,
|
|
1399
|
+
);
|
|
1400
|
+
logger.time("prewarmCodex:establishWs");
|
|
1401
|
+
await getOrCreateCodexWebSocketConnection(state, toWebSocketUrl(url), headers, options?.signal);
|
|
1402
|
+
state.prewarmed = true;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
function getCodexWebSocketSessionKey(
|
|
1406
|
+
sessionId: string | undefined,
|
|
1407
|
+
model: Model<"openai-codex-responses">,
|
|
1408
|
+
accountId: string,
|
|
1409
|
+
baseUrl: string,
|
|
1410
|
+
): string | undefined {
|
|
1411
|
+
if (!sessionId || sessionId.length === 0) return undefined;
|
|
1412
|
+
return `${accountId}:${baseUrl}:${model.id}:${sessionId}`;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
function getCodexPublicSessionKey(
|
|
1416
|
+
sessionId: string | undefined,
|
|
1417
|
+
model: Model<"openai-codex-responses">,
|
|
1418
|
+
baseUrl: string,
|
|
1419
|
+
): string | undefined {
|
|
1420
|
+
if (!sessionId || sessionId.length === 0) return undefined;
|
|
1421
|
+
return `${baseUrl}:${model.id}:${sessionId}`;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function getCodexWebSocketSessionState(
|
|
1425
|
+
sessionKey: string,
|
|
1426
|
+
providerSessionState: CodexProviderSessionState,
|
|
1427
|
+
): CodexWebSocketSessionState {
|
|
1428
|
+
const existing = providerSessionState.webSocketSessions.get(sessionKey);
|
|
1429
|
+
if (existing) return existing;
|
|
1430
|
+
const created: CodexWebSocketSessionState = {
|
|
1431
|
+
disableWebsocket: false,
|
|
1432
|
+
canAppend: false,
|
|
1433
|
+
fallbackCount: 0,
|
|
1434
|
+
prewarmed: false,
|
|
1435
|
+
};
|
|
1436
|
+
providerSessionState.webSocketSessions.set(sessionKey, created);
|
|
1437
|
+
return created;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function resetCodexWebSocketAppendState(state: CodexWebSocketSessionState): void {
|
|
1441
|
+
state.canAppend = false;
|
|
1442
|
+
state.lastRequest = undefined;
|
|
1443
|
+
state.lastResponseId = undefined;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function resetCodexSessionMetadata(state: CodexWebSocketSessionState): void {
|
|
1447
|
+
state.turnState = undefined;
|
|
1448
|
+
state.modelsEtag = undefined;
|
|
1449
|
+
state.reasoningIncluded = undefined;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function recordCodexWebSocketFailure(state: CodexWebSocketSessionState, activateFallback: boolean): void {
|
|
1453
|
+
resetCodexWebSocketAppendState(state);
|
|
1454
|
+
state.connection?.close("fallback");
|
|
1455
|
+
state.connection = undefined;
|
|
1456
|
+
state.lastFallbackAt = Date.now();
|
|
1457
|
+
if (activateFallback && !state.disableWebsocket) {
|
|
1458
|
+
state.disableWebsocket = true;
|
|
1459
|
+
state.fallbackCount += 1;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
function shouldUseCodexWebSocket(
|
|
1464
|
+
model: Model<"openai-codex-responses">,
|
|
1465
|
+
state: CodexWebSocketSessionState | undefined,
|
|
1466
|
+
preferWebsockets?: boolean,
|
|
1467
|
+
): boolean {
|
|
1468
|
+
if (!state || state.disableWebsocket) return false;
|
|
1469
|
+
if (preferWebsockets === false) return false;
|
|
1470
|
+
return isCodexWebSocketEnvEnabled() || preferWebsockets === true || model.preferWebsockets === true;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
export interface OpenAICodexTransportDetails {
|
|
1474
|
+
websocketPreferred: boolean;
|
|
1475
|
+
lastTransport?: CodexTransport;
|
|
1476
|
+
websocketDisabled: boolean;
|
|
1477
|
+
websocketConnected: boolean;
|
|
1478
|
+
fallbackCount: number;
|
|
1479
|
+
canAppend: boolean;
|
|
1480
|
+
prewarmed: boolean;
|
|
1481
|
+
hasSessionState: boolean;
|
|
1482
|
+
lastFallbackAt?: number;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
export function getOpenAICodexTransportDetails(
|
|
1486
|
+
model: Model<"openai-codex-responses">,
|
|
1487
|
+
options?: {
|
|
1488
|
+
sessionId?: string;
|
|
1489
|
+
baseUrl?: string;
|
|
1490
|
+
preferWebsockets?: boolean;
|
|
1491
|
+
providerSessionState?: Map<string, ProviderSessionState>;
|
|
1492
|
+
},
|
|
1493
|
+
): OpenAICodexTransportDetails {
|
|
1494
|
+
const baseUrl = options?.baseUrl || model.baseUrl || CODEX_BASE_URL;
|
|
1495
|
+
const websocketPreferred =
|
|
1496
|
+
options?.preferWebsockets === false
|
|
1497
|
+
? false
|
|
1498
|
+
: isCodexWebSocketEnvEnabled() || options?.preferWebsockets === true || model.preferWebsockets === true;
|
|
1499
|
+
const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
|
|
1500
|
+
const publicSessionKey = getCodexPublicSessionKey(options?.sessionId, model, baseUrl);
|
|
1501
|
+
const privateSessionKey = publicSessionKey
|
|
1502
|
+
? providerSessionState?.webSocketPublicToPrivate.get(publicSessionKey)
|
|
1503
|
+
: undefined;
|
|
1504
|
+
const state = privateSessionKey ? providerSessionState?.webSocketSessions.get(privateSessionKey) : undefined;
|
|
1505
|
+
|
|
1506
|
+
return {
|
|
1507
|
+
websocketPreferred,
|
|
1508
|
+
lastTransport: state?.lastTransport,
|
|
1509
|
+
websocketDisabled: state?.disableWebsocket ?? false,
|
|
1510
|
+
websocketConnected: state?.connection?.isOpen() ?? false,
|
|
1511
|
+
fallbackCount: state?.fallbackCount ?? 0,
|
|
1512
|
+
canAppend: state?.canAppend ?? false,
|
|
1513
|
+
prewarmed: state?.prewarmed ?? false,
|
|
1514
|
+
hasSessionState: state !== undefined,
|
|
1515
|
+
lastFallbackAt: state?.lastFallbackAt,
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function buildAppendInput(previous: RequestBody | undefined, current: RequestBody): InputItem[] | null {
|
|
1520
|
+
if (!previous) return null;
|
|
1521
|
+
if (!Array.isArray(previous.input) || !Array.isArray(current.input)) return null;
|
|
1522
|
+
if (current.input.length <= previous.input.length) return null;
|
|
1523
|
+
const previousWithoutInput = { ...previous, input: undefined };
|
|
1524
|
+
const currentWithoutInput = { ...current, input: undefined };
|
|
1525
|
+
if (JSON.stringify(previousWithoutInput) !== JSON.stringify(currentWithoutInput)) {
|
|
1526
|
+
return null;
|
|
1527
|
+
}
|
|
1528
|
+
for (let index = 0; index < previous.input.length; index += 1) {
|
|
1529
|
+
if (JSON.stringify(previous.input[index]) !== JSON.stringify(current.input[index])) {
|
|
1530
|
+
return null;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return current.input.slice(previous.input.length) as InputItem[];
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function buildCodexWebSocketRequest(
|
|
1537
|
+
requestBody: RequestBody,
|
|
1538
|
+
state: CodexWebSocketSessionState | undefined,
|
|
1539
|
+
): Record<string, unknown> {
|
|
1540
|
+
const appendInput = state?.canAppend ? buildAppendInput(state.lastRequest, requestBody) : null;
|
|
1541
|
+
if (appendInput && appendInput.length > 0) {
|
|
1542
|
+
if (state?.lastResponseId) {
|
|
1543
|
+
return {
|
|
1544
|
+
type: "response.create",
|
|
1545
|
+
...requestBody,
|
|
1546
|
+
previous_response_id: state.lastResponseId,
|
|
1547
|
+
input: appendInput,
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
return {
|
|
1551
|
+
type: "response.append",
|
|
1552
|
+
input: appendInput,
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
if (state?.canAppend) {
|
|
1556
|
+
logCodexDebug("codex websocket append reset", {
|
|
1557
|
+
hadTurnStateHeader: Boolean(state.turnState),
|
|
1558
|
+
hadModelsEtagHeader: Boolean(state.modelsEtag),
|
|
1559
|
+
});
|
|
1560
|
+
resetCodexWebSocketAppendState(state);
|
|
1561
|
+
resetCodexSessionMetadata(state);
|
|
1562
|
+
}
|
|
1563
|
+
return {
|
|
1564
|
+
type: "response.create",
|
|
1565
|
+
...requestBody,
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
function toWebSocketUrl(url: string): string {
|
|
1570
|
+
const parsed = new URL(url);
|
|
1571
|
+
if (parsed.protocol === "https:") {
|
|
1572
|
+
parsed.protocol = "wss:";
|
|
1573
|
+
} else if (parsed.protocol === "http:") {
|
|
1574
|
+
parsed.protocol = "ws:";
|
|
1575
|
+
}
|
|
1576
|
+
return parsed.toString();
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
function headersToRecord(headers: Headers): Record<string, string> {
|
|
1580
|
+
const result: Record<string, string> = {};
|
|
1581
|
+
for (const [key, value] of headers.entries()) {
|
|
1582
|
+
result[key] = value;
|
|
1583
|
+
}
|
|
1584
|
+
return result;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
interface CodexWebSocketConnectionOptions {
|
|
1588
|
+
idleTimeoutMs: number;
|
|
1589
|
+
firstEventTimeoutMs: number;
|
|
1590
|
+
onHandshakeHeaders?: (headers: Headers) => void;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
class CodexWebSocketConnection {
|
|
1594
|
+
#url: string;
|
|
1595
|
+
#headers: Record<string, string>;
|
|
1596
|
+
#idleTimeoutMs: number;
|
|
1597
|
+
#firstEventTimeoutMs: number;
|
|
1598
|
+
#onHandshakeHeaders?: (headers: Headers) => void;
|
|
1599
|
+
#socket: WebSocket | null = null;
|
|
1600
|
+
#queue: Array<Record<string, unknown> | Error | null> = [];
|
|
1601
|
+
#waiters: Array<() => void> = [];
|
|
1602
|
+
#connectPromise?: Promise<void>;
|
|
1603
|
+
#activeRequest = false;
|
|
1604
|
+
|
|
1605
|
+
constructor(url: string, headers: Record<string, string>, options: CodexWebSocketConnectionOptions) {
|
|
1606
|
+
this.#url = url;
|
|
1607
|
+
this.#headers = headers;
|
|
1608
|
+
this.#idleTimeoutMs = options.idleTimeoutMs;
|
|
1609
|
+
this.#firstEventTimeoutMs = options.firstEventTimeoutMs;
|
|
1610
|
+
this.#onHandshakeHeaders = options.onHandshakeHeaders;
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
isOpen(): boolean {
|
|
1614
|
+
return this.#socket?.readyState === WebSocket.OPEN;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
matchesAuth(headers: Record<string, string>): boolean {
|
|
1618
|
+
return this.#headers.authorization === headers.authorization;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
close(reason = "done"): void {
|
|
1622
|
+
if (
|
|
1623
|
+
this.#socket &&
|
|
1624
|
+
(this.#socket.readyState === WebSocket.OPEN || this.#socket.readyState === WebSocket.CONNECTING)
|
|
1625
|
+
) {
|
|
1626
|
+
this.#socket.close(1000, reason);
|
|
1627
|
+
}
|
|
1628
|
+
this.#socket = null;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
async connect(signal?: AbortSignal): Promise<void> {
|
|
1632
|
+
if (this.isOpen()) return;
|
|
1633
|
+
if (this.#connectPromise) {
|
|
1634
|
+
logger.time("codexWs:awaitSharedHandshake");
|
|
1635
|
+
await this.#connectPromise;
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
const WebSocketWithHeaders = WebSocket as unknown as {
|
|
1639
|
+
new (url: string, options?: { headers?: Record<string, string> }): WebSocket;
|
|
1640
|
+
};
|
|
1641
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
1642
|
+
this.#connectPromise = promise;
|
|
1643
|
+
const socket = new WebSocketWithHeaders(this.#url, { headers: this.#headers });
|
|
1644
|
+
this.#socket = socket;
|
|
1645
|
+
let settled = false;
|
|
1646
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
1647
|
+
const onAbort = () => {
|
|
1648
|
+
socket.close(1000, "aborted");
|
|
1649
|
+
if (!settled) {
|
|
1650
|
+
settled = true;
|
|
1651
|
+
reject(createCodexWebSocketTransportError("request was aborted"));
|
|
1652
|
+
}
|
|
1653
|
+
};
|
|
1654
|
+
if (signal) {
|
|
1655
|
+
if (signal.aborted) {
|
|
1656
|
+
onAbort();
|
|
1657
|
+
} else {
|
|
1658
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
const clearPending = () => {
|
|
1662
|
+
if (timeout) clearTimeout(timeout);
|
|
1663
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
1664
|
+
};
|
|
1665
|
+
timeout = setTimeout(() => {
|
|
1666
|
+
socket.close(1000, "connect-timeout");
|
|
1667
|
+
if (!settled) {
|
|
1668
|
+
settled = true;
|
|
1669
|
+
reject(createCodexWebSocketTransportError("connection timeout"));
|
|
1670
|
+
}
|
|
1671
|
+
}, CODEX_WEBSOCKET_CONNECT_TIMEOUT_MS);
|
|
1672
|
+
|
|
1673
|
+
socket.addEventListener("open", event => {
|
|
1674
|
+
if (!settled) {
|
|
1675
|
+
settled = true;
|
|
1676
|
+
clearPending();
|
|
1677
|
+
this.#captureHandshakeHeaders(socket, event);
|
|
1678
|
+
resolve();
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
socket.addEventListener("error", event => {
|
|
1682
|
+
const eventRecord = event as unknown as Record<string, unknown>;
|
|
1683
|
+
const detail =
|
|
1684
|
+
(typeof eventRecord.message === "string" && eventRecord.message) ||
|
|
1685
|
+
(eventRecord.error instanceof Error && eventRecord.error.message) ||
|
|
1686
|
+
String(event.type);
|
|
1687
|
+
const error = createCodexWebSocketTransportError(`websocket error: ${detail}`);
|
|
1688
|
+
if (!settled) {
|
|
1689
|
+
settled = true;
|
|
1690
|
+
clearPending();
|
|
1691
|
+
reject(error);
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
this.#push(error);
|
|
1695
|
+
});
|
|
1696
|
+
socket.addEventListener("close", event => {
|
|
1697
|
+
this.#socket = null;
|
|
1698
|
+
if (!settled) {
|
|
1699
|
+
settled = true;
|
|
1700
|
+
clearPending();
|
|
1701
|
+
reject(createCodexWebSocketTransportError(`websocket closed before open (${event.code})`));
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
this.#push(createCodexWebSocketTransportError(`websocket closed (${event.code})`));
|
|
1705
|
+
this.#push(null);
|
|
1706
|
+
});
|
|
1707
|
+
socket.addEventListener("message", event => {
|
|
1708
|
+
if (typeof event.data !== "string") return;
|
|
1709
|
+
try {
|
|
1710
|
+
const parsed = JSON.parse(event.data) as Record<string, unknown>;
|
|
1711
|
+
if (parsed.type === "error" && typeof parsed.error === "object" && parsed.error) {
|
|
1712
|
+
const inner = parsed.error as Record<string, unknown>;
|
|
1713
|
+
if (typeof parsed.code !== "string" && typeof inner.code === "string") {
|
|
1714
|
+
parsed.code = inner.code;
|
|
1715
|
+
}
|
|
1716
|
+
if (typeof parsed.message !== "string" && typeof inner.message === "string") {
|
|
1717
|
+
parsed.message = inner.message;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
this.#push(parsed);
|
|
1721
|
+
} catch (error) {
|
|
1722
|
+
this.#push(createCodexWebSocketTransportError(String(error)));
|
|
1723
|
+
}
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
logger.time("codexWs:awaitTcpHandshake");
|
|
1727
|
+
try {
|
|
1728
|
+
await promise;
|
|
1729
|
+
} finally {
|
|
1730
|
+
this.#connectPromise = undefined;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
async *streamRequest(
|
|
1735
|
+
request: Record<string, unknown>,
|
|
1736
|
+
signal?: AbortSignal,
|
|
1737
|
+
): AsyncGenerator<Record<string, unknown>> {
|
|
1738
|
+
if (!this.#socket || this.#socket.readyState !== WebSocket.OPEN) {
|
|
1739
|
+
throw createCodexWebSocketTransportError("websocket connection is unavailable");
|
|
1740
|
+
}
|
|
1741
|
+
if (this.#activeRequest) {
|
|
1742
|
+
throw createCodexWebSocketTransportError("websocket request already in progress");
|
|
1743
|
+
}
|
|
1744
|
+
this.#activeRequest = true;
|
|
1745
|
+
const onAbort = () => {
|
|
1746
|
+
this.close("aborted");
|
|
1747
|
+
this.#push(createCodexWebSocketTransportError("request was aborted"));
|
|
1748
|
+
};
|
|
1749
|
+
if (signal) {
|
|
1750
|
+
if (signal.aborted) {
|
|
1751
|
+
onAbort();
|
|
1752
|
+
} else {
|
|
1753
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
try {
|
|
1758
|
+
this.#socket.send(JSON.stringify(request));
|
|
1759
|
+
let sawFirstEvent = false;
|
|
1760
|
+
while (true) {
|
|
1761
|
+
const next = await this.#nextMessage(
|
|
1762
|
+
sawFirstEvent ? this.#idleTimeoutMs : this.#firstEventTimeoutMs,
|
|
1763
|
+
sawFirstEvent ? "idle timeout waiting for websocket" : "timeout waiting for first websocket event",
|
|
1764
|
+
);
|
|
1765
|
+
if (next instanceof Error) {
|
|
1766
|
+
throw next;
|
|
1767
|
+
}
|
|
1768
|
+
if (next === null) {
|
|
1769
|
+
throw createCodexWebSocketTransportError("websocket closed before response completion");
|
|
1770
|
+
}
|
|
1771
|
+
sawFirstEvent = true;
|
|
1772
|
+
yield next;
|
|
1773
|
+
const eventType = typeof next.type === "string" ? next.type : "";
|
|
1774
|
+
if (
|
|
1775
|
+
eventType === "response.completed" ||
|
|
1776
|
+
eventType === "response.done" ||
|
|
1777
|
+
eventType === "response.incomplete" ||
|
|
1778
|
+
eventType === "response.failed" ||
|
|
1779
|
+
eventType === "error"
|
|
1780
|
+
) {
|
|
1781
|
+
break;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
} finally {
|
|
1785
|
+
this.#activeRequest = false;
|
|
1786
|
+
if (signal) {
|
|
1787
|
+
signal.removeEventListener("abort", onAbort);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
#captureHandshakeHeaders(socket: WebSocket, openEvent?: Event): void {
|
|
1793
|
+
if (!this.#onHandshakeHeaders) return;
|
|
1794
|
+
const headers = extractCodexWebSocketHandshakeHeaders(socket, openEvent);
|
|
1795
|
+
if (!headers) return;
|
|
1796
|
+
this.#onHandshakeHeaders(headers);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
#push(item: Record<string, unknown> | Error | null): void {
|
|
1800
|
+
this.#queue.push(item);
|
|
1801
|
+
const waiter = this.#waiters.shift();
|
|
1802
|
+
if (waiter) waiter();
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
async #nextMessage(timeoutMs: number, timeoutReason: string): Promise<Record<string, unknown> | Error | null> {
|
|
1806
|
+
while (this.#queue.length === 0) {
|
|
1807
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
1808
|
+
this.#waiters.push(resolve);
|
|
1809
|
+
let timedOut = false;
|
|
1810
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
1811
|
+
if (timeoutMs > 0) {
|
|
1812
|
+
timeout = setTimeout(() => {
|
|
1813
|
+
timedOut = true;
|
|
1814
|
+
const waiterIndex = this.#waiters.indexOf(resolve);
|
|
1815
|
+
if (waiterIndex >= 0) {
|
|
1816
|
+
this.#waiters.splice(waiterIndex, 1);
|
|
1817
|
+
}
|
|
1818
|
+
resolve();
|
|
1819
|
+
}, timeoutMs);
|
|
1820
|
+
}
|
|
1821
|
+
await promise;
|
|
1822
|
+
if (timeout) clearTimeout(timeout);
|
|
1823
|
+
if (timedOut && this.#queue.length === 0) {
|
|
1824
|
+
return createCodexWebSocketTransportError(timeoutReason);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
return this.#queue.shift() ?? null;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
async function getOrCreateCodexWebSocketConnection(
|
|
1832
|
+
state: CodexWebSocketSessionState,
|
|
1833
|
+
url: string,
|
|
1834
|
+
headers: Headers,
|
|
1835
|
+
signal?: AbortSignal,
|
|
1836
|
+
): Promise<CodexWebSocketConnection> {
|
|
1837
|
+
const headerRecord = headersToRecord(headers);
|
|
1838
|
+
if (state.connection?.isOpen()) {
|
|
1839
|
+
if (state.connection.matchesAuth(headerRecord)) {
|
|
1840
|
+
logger.time("codexWs:reuseOpenSocket");
|
|
1841
|
+
return state.connection;
|
|
1842
|
+
}
|
|
1843
|
+
state.connection.close("token-refresh");
|
|
1844
|
+
resetCodexWebSocketAppendState(state);
|
|
1845
|
+
}
|
|
1846
|
+
state.connection?.close("reconnect");
|
|
1847
|
+
resetCodexWebSocketAppendState(state);
|
|
1848
|
+
logger.time("codexWs:newSocket");
|
|
1849
|
+
state.connection = new CodexWebSocketConnection(url, headerRecord, {
|
|
1850
|
+
idleTimeoutMs: getCodexWebSocketIdleTimeoutMs(),
|
|
1851
|
+
firstEventTimeoutMs: getCodexWebSocketFirstEventTimeoutMs(),
|
|
1852
|
+
onHandshakeHeaders: handshakeHeaders => {
|
|
1853
|
+
updateCodexSessionMetadataFromHeaders(state, handshakeHeaders);
|
|
1854
|
+
},
|
|
1855
|
+
});
|
|
1856
|
+
await state.connection.connect(signal);
|
|
1857
|
+
return state.connection;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
async function openCodexSseEventStream(
|
|
1861
|
+
url: string,
|
|
1862
|
+
requestHeaders: Record<string, string> | undefined,
|
|
1863
|
+
accountId: string,
|
|
1864
|
+
apiKey: string,
|
|
1865
|
+
sessionId: string | undefined,
|
|
1866
|
+
body: RequestBody,
|
|
1867
|
+
state: CodexWebSocketSessionState | undefined,
|
|
1868
|
+
signal?: AbortSignal,
|
|
1869
|
+
): Promise<AsyncGenerator<Record<string, unknown>>> {
|
|
1870
|
+
const headers = createCodexHeaders(requestHeaders, accountId, apiKey, sessionId, "sse", state);
|
|
1871
|
+
logCodexDebug("codex request", {
|
|
1872
|
+
url,
|
|
1873
|
+
model: body.model,
|
|
1874
|
+
headers: redactHeaders(headers),
|
|
1875
|
+
sentTurnStateHeader: headers.has(X_CODEX_TURN_STATE_HEADER),
|
|
1876
|
+
sentModelsEtagHeader: headers.has(X_MODELS_ETAG_HEADER),
|
|
1877
|
+
});
|
|
1878
|
+
const response = await fetchWithRetry(
|
|
1879
|
+
url,
|
|
1880
|
+
{
|
|
1881
|
+
method: "POST",
|
|
1882
|
+
headers,
|
|
1883
|
+
body: JSON.stringify(body),
|
|
1884
|
+
},
|
|
1885
|
+
signal,
|
|
1886
|
+
);
|
|
1887
|
+
logCodexDebug("codex response", {
|
|
1888
|
+
url: response.url,
|
|
1889
|
+
status: response.status,
|
|
1890
|
+
statusText: response.statusText,
|
|
1891
|
+
contentType: response.headers.get("content-type") || null,
|
|
1892
|
+
cfRay: response.headers.get("cf-ray") || null,
|
|
1893
|
+
});
|
|
1894
|
+
updateCodexSessionMetadataFromHeaders(state, response.headers);
|
|
1895
|
+
if (!response.ok) {
|
|
1896
|
+
const info = await parseCodexError(response);
|
|
1897
|
+
const error = new Error(info.friendlyMessage || info.message);
|
|
1898
|
+
(error as { headers?: Headers; status?: number }).headers = response.headers;
|
|
1899
|
+
(error as { headers?: Headers; status?: number }).status = response.status;
|
|
1900
|
+
throw error;
|
|
1901
|
+
}
|
|
1902
|
+
if (!response.body) {
|
|
1903
|
+
throw new Error("No response body");
|
|
1904
|
+
}
|
|
1905
|
+
return readSseJson<Record<string, unknown>>(response.body, signal);
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
async function openCodexWebSocketEventStream(
|
|
1909
|
+
url: string,
|
|
1910
|
+
headers: Headers,
|
|
1911
|
+
request: Record<string, unknown>,
|
|
1912
|
+
state: CodexWebSocketSessionState,
|
|
1913
|
+
signal?: AbortSignal,
|
|
1914
|
+
): Promise<AsyncGenerator<Record<string, unknown>>> {
|
|
1915
|
+
const connection = await getOrCreateCodexWebSocketConnection(state, url, headers, signal);
|
|
1916
|
+
return connection.streamRequest(request, signal);
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
function createCodexHeaders(
|
|
1920
|
+
initHeaders: Record<string, string> | undefined,
|
|
1921
|
+
accountId: string,
|
|
1922
|
+
accessToken: string,
|
|
1923
|
+
promptCacheKey?: string,
|
|
1924
|
+
transport: CodexTransport = "sse",
|
|
1925
|
+
state?: CodexWebSocketSessionState,
|
|
1926
|
+
): Headers {
|
|
1927
|
+
const headers = new Headers(initHeaders ?? {});
|
|
1928
|
+
headers.delete("x-api-key");
|
|
1929
|
+
headers.set("Authorization", `Bearer ${accessToken}`);
|
|
1930
|
+
headers.set(OPENAI_HEADERS.ACCOUNT_ID, accountId);
|
|
1931
|
+
const betaHeader =
|
|
1932
|
+
transport === "websocket"
|
|
1933
|
+
? OPENAI_HEADER_VALUES.BETA_RESPONSES_WEBSOCKETS_V2
|
|
1934
|
+
: OPENAI_HEADER_VALUES.BETA_RESPONSES;
|
|
1935
|
+
headers.set(OPENAI_HEADERS.BETA, betaHeader);
|
|
1936
|
+
headers.set(OPENAI_HEADERS.ORIGINATOR, OPENAI_HEADER_VALUES.ORIGINATOR_CODEX);
|
|
1937
|
+
headers.set("User-Agent", `pi/${packageJson.version} (${os.platform()} ${os.release()}; ${os.arch()})`);
|
|
1938
|
+
if (promptCacheKey) {
|
|
1939
|
+
headers.set(OPENAI_HEADERS.CONVERSATION_ID, promptCacheKey);
|
|
1940
|
+
headers.set(OPENAI_HEADERS.SESSION_ID, promptCacheKey);
|
|
1941
|
+
} else {
|
|
1942
|
+
headers.delete(OPENAI_HEADERS.CONVERSATION_ID);
|
|
1943
|
+
headers.delete(OPENAI_HEADERS.SESSION_ID);
|
|
1944
|
+
}
|
|
1945
|
+
if (state?.turnState) {
|
|
1946
|
+
headers.set(X_CODEX_TURN_STATE_HEADER, state.turnState);
|
|
1947
|
+
} else {
|
|
1948
|
+
headers.delete(X_CODEX_TURN_STATE_HEADER);
|
|
1949
|
+
}
|
|
1950
|
+
if (state?.modelsEtag) {
|
|
1951
|
+
headers.set(X_MODELS_ETAG_HEADER, state.modelsEtag);
|
|
1952
|
+
} else {
|
|
1953
|
+
headers.delete(X_MODELS_ETAG_HEADER);
|
|
1954
|
+
}
|
|
1955
|
+
if (transport === "sse") {
|
|
1956
|
+
headers.set("accept", "text/event-stream");
|
|
1957
|
+
} else {
|
|
1958
|
+
headers.delete("accept");
|
|
1959
|
+
}
|
|
1960
|
+
headers.set("content-type", "application/json");
|
|
1961
|
+
return headers;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
function logCodexDebug(message: string, details?: Record<string, unknown>): void {
|
|
1965
|
+
if (!CODEX_DEBUG) return;
|
|
1966
|
+
logger.debug(`[codex] ${message}`, details ?? {});
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
function getRetryDelayMs(
|
|
1970
|
+
response: Response | null,
|
|
1971
|
+
attempt: number,
|
|
1972
|
+
errorBody?: string,
|
|
1973
|
+
): { delay: number; serverProvided: boolean } {
|
|
1974
|
+
const retryAfter = response?.headers?.get("retry-after") || null;
|
|
1975
|
+
if (retryAfter) {
|
|
1976
|
+
const seconds = Number(retryAfter);
|
|
1977
|
+
if (Number.isFinite(seconds)) {
|
|
1978
|
+
return { delay: Math.max(0, seconds * 1000), serverProvided: true };
|
|
1979
|
+
}
|
|
1980
|
+
const parsedDate = Date.parse(retryAfter);
|
|
1981
|
+
if (!Number.isNaN(parsedDate)) {
|
|
1982
|
+
return { delay: Math.max(0, parsedDate - Date.now()), serverProvided: true };
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
if (errorBody) {
|
|
1986
|
+
const msMatch = /try again in\s+(\d+(?:\.\d+)?)\s*ms/i.exec(errorBody);
|
|
1987
|
+
if (msMatch) {
|
|
1988
|
+
const ms = Number(msMatch[1]);
|
|
1989
|
+
if (Number.isFinite(ms)) return { delay: Math.max(ms, 100), serverProvided: true };
|
|
1990
|
+
}
|
|
1991
|
+
const sMatch = /try again in\s+(\d+(?:\.\d+)?)\s*s(?:ec)?/i.exec(errorBody);
|
|
1992
|
+
if (sMatch) {
|
|
1993
|
+
const seconds = Number(sMatch[1]);
|
|
1994
|
+
if (Number.isFinite(seconds)) return { delay: Math.max(seconds * 1000, 100), serverProvided: true };
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
return { delay: CODEX_RETRY_DELAY_MS * (attempt + 1), serverProvided: false };
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
async function fetchWithRetry(url: string, init: RequestInit, signal?: AbortSignal): Promise<Response> {
|
|
2001
|
+
let attempt = 0;
|
|
2002
|
+
let rateLimitTimeSpent = 0;
|
|
2003
|
+
while (true) {
|
|
2004
|
+
try {
|
|
2005
|
+
const response = await fetch(url, { ...init, signal: signal ?? init.signal });
|
|
2006
|
+
if (!CODEX_RETRYABLE_STATUS.has(response.status)) {
|
|
2007
|
+
return response;
|
|
2008
|
+
}
|
|
2009
|
+
if (signal?.aborted) return response;
|
|
2010
|
+
const errorBody = await response.clone().text();
|
|
2011
|
+
// Usage-limit errors are persistent (account allocation exhausted) — retrying with the
|
|
2012
|
+
// same credential is futile. Bail out immediately so the error propagates to the agent
|
|
2013
|
+
// session layer where credential switching happens.
|
|
2014
|
+
if (response.status === 429 && isUsageLimitError(errorBody)) {
|
|
2015
|
+
return response;
|
|
2016
|
+
}
|
|
2017
|
+
const { delay, serverProvided } = getRetryDelayMs(response, attempt, errorBody);
|
|
2018
|
+
if (response.status === 429 && serverProvided) {
|
|
2019
|
+
if (rateLimitTimeSpent + delay > CODEX_RATE_LIMIT_BUDGET_MS) {
|
|
2020
|
+
return response;
|
|
2021
|
+
}
|
|
2022
|
+
rateLimitTimeSpent += delay;
|
|
2023
|
+
} else if (attempt >= CODEX_MAX_RETRIES) {
|
|
2024
|
+
return response;
|
|
2025
|
+
}
|
|
2026
|
+
await abortableSleep(delay, signal);
|
|
2027
|
+
} catch (error) {
|
|
2028
|
+
if (attempt >= CODEX_MAX_RETRIES || signal?.aborted) {
|
|
2029
|
+
throw error;
|
|
2030
|
+
}
|
|
2031
|
+
const delay = CODEX_RETRY_DELAY_MS * (attempt + 1);
|
|
2032
|
+
await abortableSleep(delay, signal);
|
|
2033
|
+
}
|
|
2034
|
+
attempt += 1;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
function redactHeaders(headers: Headers): Record<string, string> {
|
|
2039
|
+
const redacted: Record<string, string> = {};
|
|
2040
|
+
for (const [key, value] of headers.entries()) {
|
|
2041
|
+
const lower = key.toLowerCase();
|
|
2042
|
+
if (lower === "authorization") {
|
|
2043
|
+
redacted[key] = "Bearer [redacted]";
|
|
2044
|
+
continue;
|
|
2045
|
+
}
|
|
2046
|
+
if (
|
|
2047
|
+
lower.includes("account") ||
|
|
2048
|
+
lower.includes("session") ||
|
|
2049
|
+
lower.includes("conversation") ||
|
|
2050
|
+
lower === "cookie"
|
|
2051
|
+
) {
|
|
2052
|
+
redacted[key] = "[redacted]";
|
|
2053
|
+
continue;
|
|
2054
|
+
}
|
|
2055
|
+
redacted[key] = value;
|
|
2056
|
+
}
|
|
2057
|
+
return redacted;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function rewriteUrlForCodex(url: string): string {
|
|
2061
|
+
return url.replace(URL_PATHS.RESPONSES, URL_PATHS.CODEX_RESPONSES);
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
function getAccountId(accessToken: string): string {
|
|
2065
|
+
const accountId = getCodexAccountId(accessToken);
|
|
2066
|
+
if (!accountId) {
|
|
2067
|
+
throw new Error("Failed to extract accountId from token");
|
|
2068
|
+
}
|
|
2069
|
+
return accountId;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
function convertMessages(model: Model<"openai-codex-responses">, context: Context): ResponseInput {
|
|
2073
|
+
const messages: ResponseInput = [];
|
|
2074
|
+
|
|
2075
|
+
const normalizeToolCallId = (id: string): string => {
|
|
2076
|
+
if (!id.includes("|")) return id;
|
|
2077
|
+
const [callId, itemId] = id.split("|");
|
|
2078
|
+
const sanitizedCallId = callId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
2079
|
+
let sanitizedItemId = itemId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
2080
|
+
if (!sanitizedItemId.startsWith("fc")) {
|
|
2081
|
+
sanitizedItemId = `fc_${sanitizedItemId}`;
|
|
2082
|
+
}
|
|
2083
|
+
let normalizedCallId = sanitizedCallId.length > 64 ? sanitizedCallId.slice(0, 64) : sanitizedCallId;
|
|
2084
|
+
let normalizedItemId = sanitizedItemId.length > 64 ? sanitizedItemId.slice(0, 64) : sanitizedItemId;
|
|
2085
|
+
normalizedCallId = normalizedCallId.replace(/_+$/, "");
|
|
2086
|
+
normalizedItemId = normalizedItemId.replace(/_+$/, "");
|
|
2087
|
+
return `${normalizedCallId}|${normalizedItemId}`;
|
|
2088
|
+
};
|
|
2089
|
+
|
|
2090
|
+
const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
|
|
2091
|
+
let msgIndex = 0;
|
|
2092
|
+
|
|
2093
|
+
for (const msg of transformedMessages) {
|
|
2094
|
+
if (msg.role === "user" || msg.role === "developer") {
|
|
2095
|
+
const providerPayload = (msg as { providerPayload?: AssistantMessage["providerPayload"] }).providerPayload;
|
|
2096
|
+
const historyItems = getOpenAIResponsesHistoryItems(providerPayload, model.provider) as
|
|
2097
|
+
| Array<ResponseInput[number]>
|
|
2098
|
+
| undefined;
|
|
2099
|
+
if (historyItems) {
|
|
2100
|
+
messages.push(...historyItems);
|
|
2101
|
+
msgIndex += 1;
|
|
2102
|
+
continue;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
const normalizedContent = normalizeInputMessageContent(model, msg.content);
|
|
2106
|
+
if (normalizedContent.length === 0) continue;
|
|
2107
|
+
messages.push({ role: msg.role, content: normalizedContent });
|
|
2108
|
+
msgIndex += 1;
|
|
2109
|
+
continue;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
if (msg.role === "assistant") {
|
|
2113
|
+
const assistantMsg = msg as AssistantMessage;
|
|
2114
|
+
const providerPayload = getOpenAIResponsesHistoryPayload(
|
|
2115
|
+
assistantMsg.providerPayload,
|
|
2116
|
+
model.provider,
|
|
2117
|
+
assistantMsg.provider,
|
|
2118
|
+
);
|
|
2119
|
+
const historyItems = providerPayload?.items as Array<ResponseInput[number]> | undefined;
|
|
2120
|
+
if (historyItems) {
|
|
2121
|
+
if (providerPayload?.dt) {
|
|
2122
|
+
messages.push(...historyItems);
|
|
2123
|
+
} else {
|
|
2124
|
+
messages.splice(0, messages.length, ...historyItems);
|
|
2125
|
+
}
|
|
2126
|
+
msgIndex += 1;
|
|
2127
|
+
continue;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
const outputItems: ResponseInput = [];
|
|
2131
|
+
for (const block of msg.content) {
|
|
2132
|
+
if (block.type === "thinking" && msg.stopReason !== "error") {
|
|
2133
|
+
if (block.thinkingSignature) {
|
|
2134
|
+
outputItems.push(JSON.parse(block.thinkingSignature) as ResponseReasoningItem);
|
|
2135
|
+
}
|
|
2136
|
+
continue;
|
|
2137
|
+
}
|
|
2138
|
+
if (block.type === "text") {
|
|
2139
|
+
const textBlock = block as TextContent;
|
|
2140
|
+
const parsedSignature = parseTextSignature(textBlock.textSignature);
|
|
2141
|
+
let msgId = parsedSignature?.id;
|
|
2142
|
+
if (!msgId) {
|
|
2143
|
+
msgId = `msg_${msgIndex}`;
|
|
2144
|
+
} else if (msgId.length > 64) {
|
|
2145
|
+
msgId = `msg_${Bun.hash(msgId).toString(36)}`;
|
|
2146
|
+
}
|
|
2147
|
+
outputItems.push({
|
|
2148
|
+
type: "message",
|
|
2149
|
+
role: "assistant",
|
|
2150
|
+
content: [{ type: "output_text", text: textBlock.text.toWellFormed(), annotations: [] }],
|
|
2151
|
+
status: "completed",
|
|
2152
|
+
id: msgId,
|
|
2153
|
+
phase: parsedSignature?.phase,
|
|
2154
|
+
} satisfies ResponseOutputMessage);
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
2157
|
+
if (block.type === "toolCall") {
|
|
2158
|
+
const toolCall = block as ToolCall;
|
|
2159
|
+
const normalized = normalizeResponsesToolCallId(toolCall.id);
|
|
2160
|
+
outputItems.push({
|
|
2161
|
+
type: "function_call",
|
|
2162
|
+
id: normalized.itemId,
|
|
2163
|
+
call_id: normalized.callId,
|
|
2164
|
+
name: toolCall.name,
|
|
2165
|
+
arguments: JSON.stringify(toolCall.arguments),
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
if (outputItems.length > 0) {
|
|
2170
|
+
messages.push(...outputItems);
|
|
2171
|
+
}
|
|
2172
|
+
msgIndex += 1;
|
|
2173
|
+
continue;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
if (msg.role === "toolResult") {
|
|
2177
|
+
const textResult = msg.content
|
|
2178
|
+
.filter(content => content.type === "text")
|
|
2179
|
+
.map(content => content.text)
|
|
2180
|
+
.join("\n");
|
|
2181
|
+
const hasImages = msg.content.some(content => content.type === "image");
|
|
2182
|
+
const normalized = normalizeResponsesToolCallId(msg.toolCallId);
|
|
2183
|
+
messages.push({
|
|
2184
|
+
type: "function_call_output",
|
|
2185
|
+
call_id: normalized.callId,
|
|
2186
|
+
output: (textResult.length > 0 ? textResult : "(see attached image)").toWellFormed(),
|
|
2187
|
+
});
|
|
2188
|
+
if (hasImages && model.input.includes("image")) {
|
|
2189
|
+
const contentParts: ResponseInputContent[] = [
|
|
2190
|
+
{ type: "input_text", text: "Attached image(s) from tool result:" } satisfies ResponseInputText,
|
|
2191
|
+
];
|
|
2192
|
+
for (const block of msg.content) {
|
|
2193
|
+
if (block.type === "image") {
|
|
2194
|
+
contentParts.push({
|
|
2195
|
+
type: "input_image",
|
|
2196
|
+
detail: "auto",
|
|
2197
|
+
image_url: `data:${block.mimeType};base64,${block.data}`,
|
|
2198
|
+
} satisfies ResponseInputImage);
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
messages.push({ role: "user", content: contentParts });
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
msgIndex += 1;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
return messages;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
function normalizeInputMessageContent(
|
|
2212
|
+
model: Model<"openai-codex-responses">,
|
|
2213
|
+
content: string | Array<{ type: "text"; text: string } | { type: "image"; mimeType: string; data: string }>,
|
|
2214
|
+
): ResponseInputContent[] {
|
|
2215
|
+
if (typeof content === "string") {
|
|
2216
|
+
if (!content || content.trim() === "") return [];
|
|
2217
|
+
return [{ type: "input_text", text: content.toWellFormed() }];
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
const normalizedContent: ResponseInputContent[] = content.map(item => {
|
|
2221
|
+
if (item.type === "text") {
|
|
2222
|
+
return { type: "input_text", text: item.text.toWellFormed() } satisfies ResponseInputText;
|
|
2223
|
+
}
|
|
2224
|
+
return {
|
|
2225
|
+
type: "input_image",
|
|
2226
|
+
detail: "auto",
|
|
2227
|
+
image_url: `data:${item.mimeType};base64,${item.data}`,
|
|
2228
|
+
} satisfies ResponseInputImage;
|
|
2229
|
+
});
|
|
2230
|
+
|
|
2231
|
+
const maybeWithoutImages = model.input.includes("image")
|
|
2232
|
+
? normalizedContent
|
|
2233
|
+
: normalizedContent.filter(item => item.type !== "input_image");
|
|
2234
|
+
return maybeWithoutImages.filter(item => item.type !== "input_text" || item.text.trim().length > 0);
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
function convertTools(tools: Tool[]): Array<{
|
|
2238
|
+
type: "function";
|
|
2239
|
+
name: string;
|
|
2240
|
+
description: string;
|
|
2241
|
+
parameters: Record<string, unknown>;
|
|
2242
|
+
strict?: boolean;
|
|
2243
|
+
}> {
|
|
2244
|
+
return tools.map(tool => {
|
|
2245
|
+
const strict = !!(!NO_STRICT && tool.strict);
|
|
2246
|
+
const baseParameters = tool.parameters as unknown as Record<string, unknown>;
|
|
2247
|
+
const { schema: parameters, strict: effectiveStrict } = adaptSchemaForStrict(baseParameters, strict);
|
|
2248
|
+
return {
|
|
2249
|
+
type: "function",
|
|
2250
|
+
name: tool.name,
|
|
2251
|
+
description: tool.description || "",
|
|
2252
|
+
parameters,
|
|
2253
|
+
...(effectiveStrict && { strict: true }),
|
|
2254
|
+
};
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
function getString(value: unknown): string | undefined {
|
|
2259
|
+
return typeof value === "string" ? value : undefined;
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
class CodexProviderStreamError extends Error {
|
|
2263
|
+
readonly retryable: boolean;
|
|
2264
|
+
readonly code?: string;
|
|
2265
|
+
|
|
2266
|
+
constructor(message: string, retryable: boolean, code?: string) {
|
|
2267
|
+
super(message);
|
|
2268
|
+
this.name = "CodexProviderStreamError";
|
|
2269
|
+
this.retryable = retryable;
|
|
2270
|
+
this.code = code;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
function isRetryableCodexFailureEvent(rawEvent: Record<string, unknown>): boolean {
|
|
2275
|
+
const response = asRecord(rawEvent.response);
|
|
2276
|
+
const error = asRecord(rawEvent.error) ?? (response ? asRecord(response.error) : null);
|
|
2277
|
+
const code = getString(error?.code) ?? getString(error?.type) ?? getString(rawEvent.code);
|
|
2278
|
+
if (code && CODEX_RETRYABLE_EVENT_CODES.has(code.toLowerCase())) {
|
|
2279
|
+
return true;
|
|
2280
|
+
}
|
|
2281
|
+
const message = getString(error?.message) ?? getString(rawEvent.message) ?? getString(response?.message);
|
|
2282
|
+
return !!message && CODEX_RETRYABLE_EVENT_MESSAGE.test(message);
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
function createCodexProviderStreamError(rawEvent: Record<string, unknown>): CodexProviderStreamError {
|
|
2286
|
+
const code = getString(rawEvent.code) ?? "";
|
|
2287
|
+
const message = getString(rawEvent.message) ?? "";
|
|
2288
|
+
const formattedMessage =
|
|
2289
|
+
typeof rawEvent.type === "string" && rawEvent.type === "error"
|
|
2290
|
+
? formatCodexErrorEvent(rawEvent, code, message)
|
|
2291
|
+
: (formatCodexFailure(rawEvent) ?? "Codex response failed");
|
|
2292
|
+
return new CodexProviderStreamError(formattedMessage, isRetryableCodexFailureEvent(rawEvent), code || undefined);
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
function isRetryableCodexProviderError(error: unknown): boolean {
|
|
2296
|
+
return error instanceof CodexProviderStreamError && error.retryable;
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
function truncate(text: string, limit: number): string {
|
|
2300
|
+
if (text.length <= limit) return text;
|
|
2301
|
+
return `${text.slice(0, limit)}…[truncated ${text.length - limit}]`;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
function formatCodexFailure(rawEvent: Record<string, unknown>): string | null {
|
|
2305
|
+
const response = asRecord(rawEvent.response);
|
|
2306
|
+
const error = asRecord(rawEvent.error) ?? (response ? asRecord(response.error) : null);
|
|
2307
|
+
const message = getString(error?.message) ?? getString(rawEvent.message) ?? getString(response?.message);
|
|
2308
|
+
const code = getString(error?.code) ?? getString(error?.type) ?? getString(rawEvent.code);
|
|
2309
|
+
const status = getString(response?.status) ?? getString(rawEvent.status);
|
|
2310
|
+
|
|
2311
|
+
const meta: string[] = [];
|
|
2312
|
+
if (code) meta.push(`code=${code}`);
|
|
2313
|
+
if (status) meta.push(`status=${status}`);
|
|
2314
|
+
|
|
2315
|
+
if (message) {
|
|
2316
|
+
const metaText = meta.length ? ` (${meta.join(", ")})` : "";
|
|
2317
|
+
return `Codex response failed: ${message}${metaText}`;
|
|
2318
|
+
}
|
|
2319
|
+
if (meta.length) {
|
|
2320
|
+
return `Codex response failed (${meta.join(", ")})`;
|
|
2321
|
+
}
|
|
2322
|
+
try {
|
|
2323
|
+
return `Codex response failed: ${truncate(JSON.stringify(rawEvent), 800)}`;
|
|
2324
|
+
} catch {
|
|
2325
|
+
return "Codex response failed";
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
function formatCodexErrorEvent(rawEvent: Record<string, unknown>, code: string, message: string): string {
|
|
2330
|
+
const detail = formatCodexFailure(rawEvent);
|
|
2331
|
+
if (detail) {
|
|
2332
|
+
return detail.replace("response failed", "error event");
|
|
2333
|
+
}
|
|
2334
|
+
const meta: string[] = [];
|
|
2335
|
+
if (code) meta.push(`code=${code}`);
|
|
2336
|
+
if (message) meta.push(`message=${message}`);
|
|
2337
|
+
if (meta.length > 0) {
|
|
2338
|
+
return `Codex error event (${meta.join(", ")})`;
|
|
2339
|
+
}
|
|
2340
|
+
try {
|
|
2341
|
+
return `Codex error event: ${truncate(JSON.stringify(rawEvent), 800)}`;
|
|
2342
|
+
} catch {
|
|
2343
|
+
return "Codex error event";
|
|
2344
|
+
}
|
|
2345
|
+
}
|