@fleetagent/pi-ai 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +29 -24
  3. package/dist/cli.js.map +1 -1
  4. package/dist/env-api-keys.d.ts.map +1 -1
  5. package/dist/env-api-keys.js +1 -1
  6. package/dist/env-api-keys.js.map +1 -1
  7. package/dist/image-models.generated.d.ts +225 -0
  8. package/dist/image-models.generated.d.ts.map +1 -1
  9. package/dist/image-models.generated.js +227 -2
  10. package/dist/image-models.generated.js.map +1 -1
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/models.d.ts +37 -2
  15. package/dist/models.d.ts.map +1 -1
  16. package/dist/models.generated.d.ts +6120 -2593
  17. package/dist/models.generated.d.ts.map +1 -1
  18. package/dist/models.generated.js +5347 -2110
  19. package/dist/models.generated.js.map +1 -1
  20. package/dist/models.js +259 -9
  21. package/dist/models.js.map +1 -1
  22. package/dist/oauth.d.ts +6 -1
  23. package/dist/oauth.d.ts.map +1 -1
  24. package/dist/oauth.js +4 -1
  25. package/dist/oauth.js.map +1 -1
  26. package/dist/providers/amazon-bedrock.d.ts +5 -4
  27. package/dist/providers/amazon-bedrock.d.ts.map +1 -1
  28. package/dist/providers/amazon-bedrock.js +303 -330
  29. package/dist/providers/amazon-bedrock.js.map +1 -1
  30. package/dist/providers/anthropic.d.ts +5 -4
  31. package/dist/providers/anthropic.d.ts.map +1 -1
  32. package/dist/providers/anthropic.js +492 -499
  33. package/dist/providers/anthropic.js.map +1 -1
  34. package/dist/providers/azure-openai-responses.d.ts +4 -2
  35. package/dist/providers/azure-openai-responses.d.ts.map +1 -1
  36. package/dist/providers/azure-openai-responses.js +52 -35
  37. package/dist/providers/azure-openai-responses.js.map +1 -1
  38. package/dist/providers/faux.d.ts +19 -22
  39. package/dist/providers/faux.d.ts.map +1 -1
  40. package/dist/providers/faux.js +106 -70
  41. package/dist/providers/faux.js.map +1 -1
  42. package/dist/providers/github-copilot-headers.d.ts +7 -4
  43. package/dist/providers/github-copilot-headers.d.ts.map +1 -1
  44. package/dist/providers/github-copilot-headers.js.map +1 -1
  45. package/dist/providers/google-shared.d.ts +24 -5
  46. package/dist/providers/google-shared.d.ts.map +1 -1
  47. package/dist/providers/google-shared.js +139 -140
  48. package/dist/providers/google-shared.js.map +1 -1
  49. package/dist/providers/google-vertex.d.ts +3 -7
  50. package/dist/providers/google-vertex.d.ts.map +1 -1
  51. package/dist/providers/google-vertex.js +216 -219
  52. package/dist/providers/google-vertex.js.map +1 -1
  53. package/dist/providers/google.d.ts +4 -7
  54. package/dist/providers/google.d.ts.map +1 -1
  55. package/dist/providers/google.js +214 -218
  56. package/dist/providers/google.js.map +1 -1
  57. package/dist/providers/images/openrouter.d.ts.map +1 -1
  58. package/dist/providers/images/openrouter.js +37 -27
  59. package/dist/providers/images/openrouter.js.map +1 -1
  60. package/dist/providers/mistral.d.ts +7 -6
  61. package/dist/providers/mistral.d.ts.map +1 -1
  62. package/dist/providers/mistral.js +287 -265
  63. package/dist/providers/mistral.js.map +1 -1
  64. package/dist/providers/openai-codex-responses.d.ts +8 -5
  65. package/dist/providers/openai-codex-responses.d.ts.map +1 -1
  66. package/dist/providers/openai-codex-responses.js +493 -391
  67. package/dist/providers/openai-codex-responses.js.map +1 -1
  68. package/dist/providers/openai-completions.d.ts +7 -10
  69. package/dist/providers/openai-completions.d.ts.map +1 -1
  70. package/dist/providers/openai-completions.js +670 -664
  71. package/dist/providers/openai-completions.js.map +1 -1
  72. package/dist/providers/openai-responses-shared.d.ts +5 -4
  73. package/dist/providers/openai-responses-shared.d.ts.map +1 -1
  74. package/dist/providers/openai-responses-shared.js +390 -360
  75. package/dist/providers/openai-responses-shared.js.map +1 -1
  76. package/dist/providers/openai-responses.d.ts +5 -5
  77. package/dist/providers/openai-responses.d.ts.map +1 -1
  78. package/dist/providers/openai-responses.js +134 -109
  79. package/dist/providers/openai-responses.js.map +1 -1
  80. package/dist/providers/simple-options.d.ts +4 -2
  81. package/dist/providers/simple-options.d.ts.map +1 -1
  82. package/dist/providers/simple-options.js.map +1 -1
  83. package/dist/providers/transform-messages.d.ts.map +1 -1
  84. package/dist/providers/transform-messages.js +112 -137
  85. package/dist/providers/transform-messages.js.map +1 -1
  86. package/dist/stream.d.ts +0 -1
  87. package/dist/stream.d.ts.map +1 -1
  88. package/dist/stream.js +0 -1
  89. package/dist/stream.js.map +1 -1
  90. package/dist/types.d.ts +91 -63
  91. package/dist/types.d.ts.map +1 -1
  92. package/dist/types.js.map +1 -1
  93. package/dist/utils/error-body.d.ts.map +1 -1
  94. package/dist/utils/error-body.js +15 -4
  95. package/dist/utils/error-body.js.map +1 -1
  96. package/dist/utils/estimate.d.ts +4 -0
  97. package/dist/utils/estimate.d.ts.map +1 -1
  98. package/dist/utils/estimate.js +3 -1
  99. package/dist/utils/estimate.js.map +1 -1
  100. package/dist/utils/headers.d.ts +3 -0
  101. package/dist/utils/headers.d.ts.map +1 -1
  102. package/dist/utils/headers.js +21 -0
  103. package/dist/utils/headers.js.map +1 -1
  104. package/dist/utils/json-parse.d.ts.map +1 -1
  105. package/dist/utils/json-parse.js +17 -19
  106. package/dist/utils/json-parse.js.map +1 -1
  107. package/dist/utils/oauth/anthropic.d.ts +4 -11
  108. package/dist/utils/oauth/anthropic.d.ts.map +1 -1
  109. package/dist/utils/oauth/anthropic.js +100 -117
  110. package/dist/utils/oauth/anthropic.js.map +1 -1
  111. package/dist/utils/oauth/device-code.d.ts.map +1 -1
  112. package/dist/utils/oauth/device-code.js +51 -38
  113. package/dist/utils/oauth/device-code.js.map +1 -1
  114. package/dist/utils/oauth/github-copilot.d.ts +6 -9
  115. package/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  116. package/dist/utils/oauth/github-copilot.js +47 -42
  117. package/dist/utils/oauth/github-copilot.js.map +1 -1
  118. package/dist/utils/oauth/index.d.ts +8 -17
  119. package/dist/utils/oauth/index.d.ts.map +1 -1
  120. package/dist/utils/oauth/index.js +27 -23
  121. package/dist/utils/oauth/index.js.map +1 -1
  122. package/dist/utils/oauth/oauth-page.d.ts.map +1 -1
  123. package/dist/utils/oauth/oauth-page.js.map +1 -1
  124. package/dist/utils/oauth/openai-codex.d.ts +7 -12
  125. package/dist/utils/oauth/openai-codex.d.ts.map +1 -1
  126. package/dist/utils/oauth/openai-codex.js +72 -89
  127. package/dist/utils/oauth/openai-codex.js.map +1 -1
  128. package/dist/utils/oauth/pkce.d.ts +5 -4
  129. package/dist/utils/oauth/pkce.d.ts.map +1 -1
  130. package/dist/utils/oauth/pkce.js.map +1 -1
  131. package/dist/utils/oauth/types.d.ts +7 -2
  132. package/dist/utils/oauth/types.d.ts.map +1 -1
  133. package/dist/utils/oauth/types.js.map +1 -1
  134. package/dist/utils/provider-retry.d.ts +16 -0
  135. package/dist/utils/provider-retry.d.ts.map +1 -0
  136. package/dist/utils/provider-retry.js +94 -0
  137. package/dist/utils/provider-retry.js.map +1 -0
  138. package/dist/utils/retry.d.ts +14 -0
  139. package/dist/utils/retry.d.ts.map +1 -1
  140. package/dist/utils/retry.js +84 -0
  141. package/dist/utils/retry.js.map +1 -1
  142. package/dist/utils/typebox-helpers.d.ts +5 -4
  143. package/dist/utils/typebox-helpers.d.ts.map +1 -1
  144. package/dist/utils/typebox-helpers.js.map +1 -1
  145. package/dist/utils/validation.d.ts.map +1 -1
  146. package/dist/utils/validation.js +102 -99
  147. package/dist/utils/validation.js.map +1 -1
  148. package/package.json +2 -2
@@ -22,7 +22,7 @@ import { appendAssistantMessageDiagnostic, createAssistantMessageDiagnostic, for
22
22
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
23
23
  import { headersToRecord } from "../utils/headers.js";
24
24
  import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
25
- import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js";
25
+ import { convertResponsesMessages, convertResponsesTools, processResponsesStream, } from "./openai-responses-shared.js";
26
26
  import { buildBaseOptions } from "./simple-options.js";
27
27
  // ============================================================================
28
28
  // Configuration
@@ -36,6 +36,7 @@ const DEFAULT_SSE_HEADER_TIMEOUT_MS = 10_000;
36
36
  const DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS = 15_000;
37
37
  const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
38
38
  const WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009;
39
+ const PREVIOUS_RESPONSE_NOT_FOUND_CODE = "previous_response_not_found";
39
40
  const CODEX_RESPONSE_STATUSES = new Set([
40
41
  "completed",
41
42
  "incomplete",
@@ -44,6 +45,14 @@ const CODEX_RESPONSE_STATUSES = new Set([
44
45
  "queued",
45
46
  "in_progress",
46
47
  ]);
48
+ function assertSuccessfulOutput(output) {
49
+ if (output.stopReason === "pending") {
50
+ throw new Error("Codex stream ended without a stop reason");
51
+ }
52
+ if (output.stopReason === "error" || output.stopReason === "aborted") {
53
+ throw new Error(output.errorMessage || "An unknown error occurred");
54
+ }
55
+ }
47
56
  // ============================================================================
48
57
  // Retry Helpers
49
58
  // ============================================================================
@@ -81,9 +90,14 @@ function getRetryAfterDelayMs(headers) {
81
90
  }
82
91
  return undefined;
83
92
  }
84
- function capRetryDelayMs(delayMs, options) {
93
+ class RetryDelayExceededError extends Error {
94
+ }
95
+ function validateRetryDelayMs(delayMs, options) {
85
96
  const maxRetryDelayMs = options?.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
86
- return maxRetryDelayMs > 0 ? Math.min(delayMs, maxRetryDelayMs) : delayMs;
97
+ if (maxRetryDelayMs > 0 && delayMs > maxRetryDelayMs) {
98
+ throw new RetryDelayExceededError(`Server requested ${Math.ceil(delayMs / 1000)}s retry delay (max: ${Math.ceil(maxRetryDelayMs / 1000)}s)`);
99
+ }
100
+ return delayMs;
87
101
  }
88
102
  function sleep(ms, signal) {
89
103
  return new Promise((resolve, reject) => {
@@ -124,176 +138,224 @@ function createSSEHeaderTimeout() {
124
138
  error: () => error,
125
139
  };
126
140
  }
141
+ function createCodexAssistantMessage(model) {
142
+ return {
143
+ role: "assistant",
144
+ content: [],
145
+ api: "openai-codex-responses",
146
+ provider: model.provider,
147
+ model: model.id,
148
+ usage: {
149
+ input: 0,
150
+ output: 0,
151
+ cacheRead: 0,
152
+ cacheWrite: 0,
153
+ totalTokens: 0,
154
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
155
+ },
156
+ stopReason: "pending",
157
+ timestamp: Date.now(),
158
+ };
159
+ }
160
+ async function prepareCodexRequest(model, context, options) {
161
+ const apiKey = options?.apiKey;
162
+ if (!apiKey)
163
+ throw new Error(`No API key for provider: ${model.provider}`);
164
+ const accountId = extractAccountId(apiKey);
165
+ let body = buildRequestBody(model, context, options);
166
+ const nextBody = await options?.onPayload?.(body, model);
167
+ if (nextBody !== undefined)
168
+ body = nextBody;
169
+ const websocketRequestId = options?.sessionId || createCodexRequestId();
170
+ const sseHeaders = buildSSEHeaders(model.headers, options?.headers, accountId, apiKey, options?.sessionId);
171
+ const websocketHeaders = buildWebSocketHeaders(model.headers, options?.headers, accountId, apiKey, websocketRequestId);
172
+ const transport = options?.transport || "auto";
173
+ const websocketDisabledForSession = transport !== "sse" && isWebSocketSseFallbackActive(options?.sessionId);
174
+ if (websocketDisabledForSession)
175
+ recordWebSocketSseFallback(options?.sessionId);
176
+ return {
177
+ body,
178
+ bodyJson: JSON.stringify(body),
179
+ sseHeaders,
180
+ websocketHeaders,
181
+ idleTimeoutMs: normalizeTimeoutMs(options?.timeoutMs),
182
+ websocketConnectTimeoutMs: normalizeTimeoutMs(options?.websocketConnectTimeoutMs),
183
+ transport,
184
+ websocketDisabledForSession,
185
+ };
186
+ }
187
+ function throwIfCodexRequestAborted(signal) {
188
+ if (signal?.aborted)
189
+ throw new Error("Request was aborted");
190
+ }
191
+ function completeCodexStream(output, stream) {
192
+ stream.push({ type: "done", reason: output.stopReason, message: output });
193
+ stream.end();
194
+ }
195
+ function failCodexStream(output, stream, options, error) {
196
+ for (const block of output.content) {
197
+ // partialJson is only a streaming scratch buffer; never persist it.
198
+ delete block.partialJson;
199
+ }
200
+ output.stopReason = options?.signal?.aborted ? "aborted" : "error";
201
+ output.errorMessage = error instanceof Error ? error.message : String(error);
202
+ stream.push({ type: "error", reason: output.stopReason, error: output });
203
+ stream.end();
204
+ }
205
+ function handleCodexWebSocketFailure(execution, error, streamStarted, retriedMissingContinuation) {
206
+ const { options, output, request } = execution;
207
+ const aborted = options?.signal?.aborted;
208
+ if (!aborted && !streamStarted && isPreviousResponseNotFoundError(error) && !retriedMissingContinuation) {
209
+ return "retry";
210
+ }
211
+ if (aborted || isCodexNonTransportError(error))
212
+ throw error;
213
+ appendAssistantMessageDiagnostic(output, createAssistantMessageDiagnostic("provider_transport_failure", error, {
214
+ configuredTransport: request.transport,
215
+ fallbackTransport: streamStarted ? undefined : "sse",
216
+ eventsEmitted: streamStarted,
217
+ phase: streamStarted ? "after_message_stream_start" : "before_message_stream_start",
218
+ requestBytes: new TextEncoder().encode(request.bodyJson).byteLength,
219
+ }));
220
+ recordWebSocketFailure(options?.sessionId, error);
221
+ if (streamStarted)
222
+ throw error;
223
+ recordWebSocketSseFallback(options?.sessionId);
224
+ return "fallback";
225
+ }
226
+ async function tryCodexWebSocketTransport(execution) {
227
+ const { model, options, output, stream, request } = execution;
228
+ if (request.transport === "sse" || request.websocketDisabledForSession)
229
+ return false;
230
+ let retriedMissingContinuation = false;
231
+ while (true) {
232
+ let streamStarted = false;
233
+ try {
234
+ await processWebSocketStream(resolveCodexWebSocketUrl(model.baseUrl), request.body, request.websocketHeaders, output, stream, model, () => {
235
+ streamStarted = true;
236
+ }, request.idleTimeoutMs, request.websocketConnectTimeoutMs, options);
237
+ throwIfCodexRequestAborted(options?.signal);
238
+ assertSuccessfulOutput(output);
239
+ completeCodexStream(output, stream);
240
+ return true;
241
+ }
242
+ catch (error) {
243
+ const action = handleCodexWebSocketFailure(execution, error, streamStarted, retriedMissingContinuation);
244
+ if (action === "retry") {
245
+ retriedMissingContinuation = true;
246
+ continue;
247
+ }
248
+ return false;
249
+ }
250
+ }
251
+ }
252
+ async function fetchCodexSseAttempt(execution) {
253
+ const { model, options, request } = execution;
254
+ const headerTimeout = createSSEHeaderTimeout();
255
+ const combinedSignal = combineAbortSignals([options?.signal, headerTimeout.signal]);
256
+ let response;
257
+ try {
258
+ try {
259
+ response = await fetch(resolveCodexUrl(model.baseUrl), {
260
+ method: "POST",
261
+ headers: request.sseHeaders,
262
+ body: request.bodyJson,
263
+ signal: combinedSignal.signal,
264
+ });
265
+ }
266
+ catch (error) {
267
+ const timeoutError = headerTimeout.error();
268
+ throw timeoutError && !options?.signal?.aborted ? timeoutError : error;
269
+ }
270
+ }
271
+ finally {
272
+ combinedSignal.cleanup();
273
+ headerTimeout.clear();
274
+ }
275
+ await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
276
+ return response;
277
+ }
278
+ function normalizeCodexSseError(error) {
279
+ if (error instanceof Error && (error.name === "AbortError" || error.message === "Request was aborted")) {
280
+ return new Error("Request was aborted");
281
+ }
282
+ return error instanceof Error ? error : new Error(String(error));
283
+ }
284
+ async function handleCodexHttpFailure(response, attempt, maxRetries, options) {
285
+ const errorText = await response.text();
286
+ if (attempt < maxRetries && isRetryableError(response.status, errorText)) {
287
+ const retryAfterDelayMs = getRetryAfterDelayMs(response.headers);
288
+ const delayMs = retryAfterDelayMs === undefined
289
+ ? BASE_DELAY_MS * 2 ** attempt
290
+ : validateRetryDelayMs(retryAfterDelayMs, options);
291
+ await sleep(delayMs, options?.signal);
292
+ return;
293
+ }
294
+ const fakeResponse = new Response(errorText, {
295
+ status: response.status,
296
+ statusText: response.statusText,
297
+ });
298
+ const info = await parseErrorResponse(fakeResponse);
299
+ throw new Error(info.friendlyMessage || info.message);
300
+ }
301
+ async function handleCodexSseAttemptError(error, attempt, maxRetries, options) {
302
+ const requestError = normalizeCodexSseError(error);
303
+ if (requestError.message === "Request was aborted")
304
+ throw requestError;
305
+ if (attempt < maxRetries &&
306
+ !(requestError instanceof RetryDelayExceededError) &&
307
+ !requestError.message.includes("usage limit")) {
308
+ await sleep(BASE_DELAY_MS * 2 ** attempt, options?.signal);
309
+ return;
310
+ }
311
+ throw requestError;
312
+ }
313
+ async function fetchCodexSseResponse(execution) {
314
+ const { options } = execution;
315
+ const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
316
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
317
+ throwIfCodexRequestAborted(options?.signal);
318
+ try {
319
+ const response = await fetchCodexSseAttempt(execution);
320
+ if (response.ok)
321
+ return response;
322
+ await handleCodexHttpFailure(response, attempt, maxRetries, options);
323
+ }
324
+ catch (error) {
325
+ await handleCodexSseAttemptError(error, attempt, maxRetries, options);
326
+ }
327
+ }
328
+ throw new Error("Failed after retries");
329
+ }
330
+ async function processCodexSseTransport(execution, response) {
331
+ const { model, options, output, stream } = execution;
332
+ if (!response.body)
333
+ throw new Error("No response body");
334
+ stream.push({ type: "start", partial: output });
335
+ await processStream(response, output, stream, model, options);
336
+ throwIfCodexRequestAborted(options?.signal);
337
+ assertSuccessfulOutput(output);
338
+ completeCodexStream(output, stream);
339
+ }
340
+ async function runCodexStream(execution) {
341
+ if (await tryCodexWebSocketTransport(execution))
342
+ return;
343
+ const response = await fetchCodexSseResponse(execution);
344
+ await processCodexSseTransport(execution, response);
345
+ }
127
346
  // ============================================================================
128
347
  // Main Stream Function
129
348
  // ============================================================================
130
349
  export const streamOpenAICodexResponses = (model, context, options) => {
131
350
  const stream = new AssistantMessageEventStream();
351
+ const output = createCodexAssistantMessage(model);
132
352
  (async () => {
133
- const output = {
134
- role: "assistant",
135
- content: [],
136
- api: "openai-codex-responses",
137
- provider: model.provider,
138
- model: model.id,
139
- usage: {
140
- input: 0,
141
- output: 0,
142
- cacheRead: 0,
143
- cacheWrite: 0,
144
- totalTokens: 0,
145
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
146
- },
147
- stopReason: "stop",
148
- timestamp: Date.now(),
149
- };
150
353
  try {
151
- const apiKey = options?.apiKey;
152
- if (!apiKey) {
153
- throw new Error(`No API key for provider: ${model.provider}`);
154
- }
155
- const accountId = extractAccountId(apiKey);
156
- let body = buildRequestBody(model, context, options);
157
- const nextBody = await options?.onPayload?.(body, model);
158
- if (nextBody !== undefined) {
159
- body = nextBody;
160
- }
161
- const websocketRequestId = options?.sessionId || createCodexRequestId();
162
- const sseHeaders = buildSSEHeaders(model.headers, options?.headers, accountId, apiKey, options?.sessionId);
163
- const websocketHeaders = buildWebSocketHeaders(model.headers, options?.headers, accountId, apiKey, websocketRequestId);
164
- const bodyJson = JSON.stringify(body);
165
- const idleTimeoutMs = normalizeTimeoutMs(options?.timeoutMs);
166
- const websocketConnectTimeoutMs = normalizeTimeoutMs(options?.websocketConnectTimeoutMs);
167
- const transport = options?.transport || "auto";
168
- const websocketDisabledForSession = transport !== "sse" && isWebSocketSseFallbackActive(options?.sessionId);
169
- if (websocketDisabledForSession) {
170
- recordWebSocketSseFallback(options?.sessionId);
171
- }
172
- if (transport !== "sse" && !websocketDisabledForSession) {
173
- let websocketStarted = false;
174
- try {
175
- await processWebSocketStream(resolveCodexWebSocketUrl(model.baseUrl), body, websocketHeaders, output, stream, model, () => {
176
- websocketStarted = true;
177
- }, idleTimeoutMs, websocketConnectTimeoutMs, options);
178
- if (options?.signal?.aborted) {
179
- throw new Error("Request was aborted");
180
- }
181
- stream.push({
182
- type: "done",
183
- reason: output.stopReason,
184
- message: output,
185
- });
186
- stream.end();
187
- return;
188
- }
189
- catch (error) {
190
- const aborted = options?.signal?.aborted;
191
- if (aborted || isCodexNonTransportError(error)) {
192
- throw error;
193
- }
194
- appendAssistantMessageDiagnostic(output, createAssistantMessageDiagnostic("provider_transport_failure", error, {
195
- configuredTransport: transport,
196
- fallbackTransport: websocketStarted ? undefined : "sse",
197
- eventsEmitted: websocketStarted,
198
- phase: websocketStarted ? "after_message_stream_start" : "before_message_stream_start",
199
- requestBytes: new TextEncoder().encode(bodyJson).byteLength,
200
- }));
201
- recordWebSocketFailure(options?.sessionId, error);
202
- if (websocketStarted) {
203
- throw error;
204
- }
205
- recordWebSocketSseFallback(options?.sessionId);
206
- }
207
- }
208
- // Fetch with retry logic for rate limits and transient errors
209
- let response;
210
- let lastError;
211
- const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
212
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
213
- if (options?.signal?.aborted) {
214
- throw new Error("Request was aborted");
215
- }
216
- try {
217
- const headerTimeout = createSSEHeaderTimeout();
218
- const combinedSignal = combineAbortSignals([options?.signal, headerTimeout.signal]);
219
- try {
220
- response = await fetch(resolveCodexUrl(model.baseUrl), {
221
- method: "POST",
222
- headers: sseHeaders,
223
- body: bodyJson,
224
- signal: combinedSignal.signal,
225
- });
226
- }
227
- catch (error) {
228
- const timeoutError = headerTimeout.error();
229
- throw timeoutError && !options?.signal?.aborted ? timeoutError : error;
230
- }
231
- finally {
232
- combinedSignal.cleanup();
233
- headerTimeout.clear();
234
- }
235
- await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
236
- if (response.ok) {
237
- break;
238
- }
239
- const errorText = await response.text();
240
- if (attempt < maxRetries && isRetryableError(response.status, errorText)) {
241
- const retryAfterDelayMs = getRetryAfterDelayMs(response.headers);
242
- const delayMs = retryAfterDelayMs === undefined
243
- ? BASE_DELAY_MS * 2 ** attempt
244
- : response.status === 429
245
- ? capRetryDelayMs(retryAfterDelayMs, options)
246
- : retryAfterDelayMs;
247
- await sleep(delayMs, options?.signal);
248
- continue;
249
- }
250
- // Parse error for friendly message on final attempt or non-retryable error
251
- const fakeResponse = new Response(errorText, {
252
- status: response.status,
253
- statusText: response.statusText,
254
- });
255
- const info = await parseErrorResponse(fakeResponse);
256
- throw new Error(info.friendlyMessage || info.message);
257
- }
258
- catch (error) {
259
- if (error instanceof Error) {
260
- if (error.name === "AbortError" || error.message === "Request was aborted") {
261
- throw new Error("Request was aborted");
262
- }
263
- }
264
- lastError = error instanceof Error ? error : new Error(String(error));
265
- // Network errors are retryable
266
- if (attempt < maxRetries && !lastError.message.includes("usage limit")) {
267
- const delayMs = BASE_DELAY_MS * 2 ** attempt;
268
- await sleep(delayMs, options?.signal);
269
- continue;
270
- }
271
- throw lastError;
272
- }
273
- }
274
- if (!response?.ok) {
275
- throw lastError ?? new Error("Failed after retries");
276
- }
277
- if (!response.body) {
278
- throw new Error("No response body");
279
- }
280
- stream.push({ type: "start", partial: output });
281
- await processStream(response, output, stream, model, options);
282
- if (options?.signal?.aborted) {
283
- throw new Error("Request was aborted");
284
- }
285
- stream.push({ type: "done", reason: output.stopReason, message: output });
286
- stream.end();
354
+ const request = await prepareCodexRequest(model, context, options);
355
+ await runCodexStream({ model, options, output, stream, request });
287
356
  }
288
357
  catch (error) {
289
- for (const block of output.content) {
290
- // partialJson is only a streaming scratch buffer; never persist it.
291
- delete block.partialJson;
292
- }
293
- output.stopReason = options?.signal?.aborted ? "aborted" : "error";
294
- output.errorMessage = error instanceof Error ? error.message : String(error);
295
- stream.push({ type: "error", reason: output.stopReason, error: output });
296
- stream.end();
358
+ failCodexStream(output, stream, options, error);
297
359
  }
298
360
  })();
299
361
  return stream;
@@ -428,25 +490,41 @@ class CodexProtocolError extends Error {
428
490
  function isCodexNonTransportError(error) {
429
491
  return error instanceof CodexApiError || error instanceof CodexProtocolError;
430
492
  }
493
+ function isPreviousResponseNotFoundError(error) {
494
+ return error instanceof CodexApiError && error.code === PREVIOUS_RESPONSE_NOT_FOUND_CODE;
495
+ }
496
+ function extractCodexEventError(event) {
497
+ const nested = event.error && typeof event.error === "object" ? event.error : undefined;
498
+ return {
499
+ code: typeof event.code === "string" ? event.code : typeof nested?.code === "string" ? nested.code : undefined,
500
+ message: typeof event.message === "string"
501
+ ? event.message
502
+ : typeof nested?.message === "string"
503
+ ? nested.message
504
+ : undefined,
505
+ };
506
+ }
507
+ function throwIfCodexEventFailed(type, event) {
508
+ if (type === "error") {
509
+ const { code, message } = extractCodexEventError(event);
510
+ throw new CodexApiError(`Codex error: ${message || code || JSON.stringify(event)}`, {
511
+ code,
512
+ payload: event,
513
+ });
514
+ }
515
+ if (type === "response.failed") {
516
+ const response = event.response;
517
+ const code = response?.error?.code;
518
+ const message = response?.error?.message;
519
+ throw new CodexApiError(message || "Codex response failed", { code, payload: event });
520
+ }
521
+ }
431
522
  async function* mapCodexEvents(events) {
432
523
  for await (const event of events) {
433
524
  const type = typeof event.type === "string" ? event.type : undefined;
434
525
  if (!type)
435
526
  continue;
436
- if (type === "error") {
437
- const code = event.code || "";
438
- const message = event.message || "";
439
- throw new CodexApiError(`Codex error: ${message || code || JSON.stringify(event)}`, {
440
- code: code || undefined,
441
- payload: event,
442
- });
443
- }
444
- if (type === "response.failed") {
445
- const response = event.response;
446
- const code = response?.error?.code;
447
- const message = response?.error?.message;
448
- throw new CodexApiError(message || "Codex response failed", { code, payload: event });
449
- }
527
+ throwIfCodexEventFailed(type, event);
450
528
  if (type === "response.done" || type === "response.completed" || type === "response.incomplete") {
451
529
  const response = event.response;
452
530
  const normalizedResponse = response
@@ -466,6 +544,39 @@ function normalizeCodexStatus(status) {
466
544
  // ============================================================================
467
545
  // SSE Parsing
468
546
  // ============================================================================
547
+ function parseCodexSseFrame(frame) {
548
+ const dataLines = frame
549
+ .split(/\r\n|\r|\n/)
550
+ .filter((line) => line.startsWith("data:"))
551
+ .map((line) => line.slice(5).trim());
552
+ if (dataLines.length === 0)
553
+ return undefined;
554
+ const data = dataLines.join("\n").trim();
555
+ if (!data || data === "[DONE]")
556
+ return undefined;
557
+ try {
558
+ return JSON.parse(data);
559
+ }
560
+ catch (cause) {
561
+ throw new CodexProtocolError(`Invalid Codex SSE JSON: ${formatThrownValue(cause)}`, {
562
+ cause,
563
+ payload: data,
564
+ });
565
+ }
566
+ }
567
+ function* decodeCodexSseFrames(buffer) {
568
+ const separator = /(?:\r\n|\r|\n)(?:\r\n|\r|\n)/;
569
+ let match = separator.exec(buffer);
570
+ while (match) {
571
+ const frame = buffer.slice(0, match.index);
572
+ buffer = buffer.slice(match.index + match[0].length);
573
+ const event = parseCodexSseFrame(frame);
574
+ if (event)
575
+ yield event;
576
+ match = separator.exec(buffer);
577
+ }
578
+ return buffer;
579
+ }
469
580
  async function* parseSSE(response, signal) {
470
581
  if (!response.body)
471
582
  return;
@@ -478,41 +589,13 @@ async function* parseSSE(response, signal) {
478
589
  signal?.addEventListener("abort", onAbort, { once: true });
479
590
  try {
480
591
  while (true) {
481
- if (signal?.aborted) {
482
- throw new Error("Request was aborted");
483
- }
592
+ throwIfCodexRequestAborted(signal);
484
593
  const { done, value } = await reader.read();
485
- if (signal?.aborted) {
486
- throw new Error("Request was aborted");
487
- }
594
+ throwIfCodexRequestAborted(signal);
488
595
  if (done)
489
596
  break;
490
597
  buffer += decoder.decode(value, { stream: true });
491
- const separator = /(?:\r\n|\r|\n)(?:\r\n|\r|\n)/;
492
- let match = separator.exec(buffer);
493
- while (match) {
494
- const chunk = buffer.slice(0, match.index);
495
- buffer = buffer.slice(match.index + match[0].length);
496
- const dataLines = chunk
497
- .split(/\r\n|\r|\n/)
498
- .filter((line) => line.startsWith("data:"))
499
- .map((line) => line.slice(5).trim());
500
- if (dataLines.length > 0) {
501
- const data = dataLines.join("\n").trim();
502
- if (data && data !== "[DONE]") {
503
- try {
504
- yield JSON.parse(data);
505
- }
506
- catch (cause) {
507
- throw new CodexProtocolError(`Invalid Codex SSE JSON: ${formatThrownValue(cause)}`, {
508
- cause,
509
- payload: data,
510
- });
511
- }
512
- }
513
- }
514
- match = separator.exec(buffer);
515
- }
598
+ buffer = (yield* decodeCodexSseFrames(buffer));
516
599
  }
517
600
  }
518
601
  finally {
@@ -830,42 +913,46 @@ async function acquireWebSocket(url, headers, sessionId, signal, connectTimeoutM
830
913
  },
831
914
  };
832
915
  }
916
+ function extractNonemptyErrorMessage(value) {
917
+ if (!value || typeof value !== "object" || !("message" in value))
918
+ return undefined;
919
+ const message = value.message;
920
+ return typeof message === "string" && message.length > 0 ? message : undefined;
921
+ }
833
922
  function extractWebSocketError(event) {
834
- if (event && typeof event === "object") {
835
- const message = "message" in event ? event.message : undefined;
836
- if (typeof message === "string" && message.length > 0) {
837
- return new Error(message);
838
- }
839
- const nestedError = "error" in event ? event.error : undefined;
840
- if (nestedError instanceof Error && nestedError.message.length > 0) {
841
- return nestedError;
842
- }
843
- if (nestedError && typeof nestedError === "object" && "message" in nestedError) {
844
- const nestedMessage = nestedError.message;
845
- if (typeof nestedMessage === "string" && nestedMessage.length > 0) {
846
- return new Error(nestedMessage);
847
- }
848
- }
849
- }
850
- return new Error("WebSocket error");
923
+ const message = extractNonemptyErrorMessage(event);
924
+ if (message)
925
+ return new Error(message);
926
+ if (!event || typeof event !== "object" || !("error" in event))
927
+ return new Error("WebSocket error");
928
+ const nestedError = event.error;
929
+ if (nestedError instanceof Error && nestedError.message.length > 0)
930
+ return nestedError;
931
+ const nestedMessage = extractNonemptyErrorMessage(nestedError);
932
+ return nestedMessage ? new Error(nestedMessage) : new Error("WebSocket error");
933
+ }
934
+ function readWebSocketCloseErrorOptions(event) {
935
+ const code = "code" in event ? event.code : undefined;
936
+ const reason = "reason" in event ? event.reason : undefined;
937
+ const wasClean = "wasClean" in event ? event.wasClean : undefined;
938
+ return {
939
+ code: typeof code === "number" ? code : undefined,
940
+ reason: typeof reason === "string" && reason.length > 0 ? reason : undefined,
941
+ wasClean: typeof wasClean === "boolean" ? wasClean : undefined,
942
+ };
943
+ }
944
+ function formatWebSocketCloseReason(options) {
945
+ if (options.reason)
946
+ return ` ${options.reason}`;
947
+ return options.code === WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE ? " message too big" : "";
851
948
  }
852
949
  function extractWebSocketCloseError(event) {
853
- if (event && typeof event === "object") {
854
- const code = "code" in event ? event.code : undefined;
855
- const reason = "reason" in event ? event.reason : undefined;
856
- const wasClean = "wasClean" in event ? event.wasClean : undefined;
857
- const codeText = typeof code === "number" ? ` ${code}` : "";
858
- let reasonText = typeof reason === "string" && reason.length > 0 ? ` ${reason}` : "";
859
- if (!reasonText && code === WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE) {
860
- reasonText = " message too big";
861
- }
862
- return new WebSocketCloseError(`WebSocket closed${codeText}${reasonText}`.trim(), {
863
- code: typeof code === "number" ? code : undefined,
864
- reason: typeof reason === "string" && reason.length > 0 ? reason : undefined,
865
- wasClean: typeof wasClean === "boolean" ? wasClean : undefined,
866
- });
867
- }
868
- return new Error("WebSocket closed");
950
+ if (!event || typeof event !== "object")
951
+ return new Error("WebSocket closed");
952
+ const options = readWebSocketCloseErrorOptions(event);
953
+ const codeText = options.code === undefined ? "" : ` ${options.code}`;
954
+ const reasonText = formatWebSocketCloseReason(options);
955
+ return new WebSocketCloseError(`WebSocket closed${codeText}${reasonText}`.trim(), options);
869
956
  }
870
957
  async function decodeWebSocketData(data) {
871
958
  if (typeof data === "string")
@@ -884,122 +971,128 @@ async function decodeWebSocketData(data) {
884
971
  }
885
972
  return null;
886
973
  }
887
- async function* parseWebSocket(socket, signal, idleTimeoutMs) {
888
- const queue = [];
889
- let pending = null;
890
- let done = false;
891
- let failed = null;
892
- let sawCompletion = false;
893
- const wake = () => {
894
- if (!pending)
895
- return;
896
- const resolve = pending;
897
- pending = null;
898
- resolve();
899
- };
900
- const enqueue = (processEvent) => {
901
- processing = processing.then(processEvent).catch((cause) => {
902
- failed =
903
- cause instanceof CodexProtocolError
904
- ? cause
905
- : new CodexProtocolError(`Invalid Codex WebSocket JSON: ${formatThrownValue(cause)}`, { cause });
906
- done = true;
907
- wake();
974
+ function wakeWebSocketParser(state) {
975
+ if (!state.pending)
976
+ return;
977
+ const resolve = state.pending;
978
+ state.pending = null;
979
+ resolve();
980
+ }
981
+ function enqueueWebSocketEvent(state, processEvent) {
982
+ state.processing = state.processing.then(processEvent).catch((cause) => {
983
+ state.failed =
984
+ cause instanceof CodexProtocolError
985
+ ? cause
986
+ : new CodexProtocolError(`Invalid Codex WebSocket JSON: ${formatThrownValue(cause)}`, { cause });
987
+ state.done = true;
988
+ wakeWebSocketParser(state);
989
+ });
990
+ }
991
+ function parseWebSocketMessageJson(text) {
992
+ try {
993
+ return JSON.parse(text);
994
+ }
995
+ catch (cause) {
996
+ throw new CodexProtocolError(`Invalid Codex WebSocket JSON: ${formatThrownValue(cause)}`, {
997
+ cause,
998
+ payload: text,
908
999
  });
1000
+ }
1001
+ }
1002
+ async function processWebSocketMessage(event, state) {
1003
+ if (!event || typeof event !== "object" || !("data" in event))
1004
+ return;
1005
+ const text = await decodeWebSocketData(event.data);
1006
+ if (!text)
1007
+ return;
1008
+ const parsed = parseWebSocketMessageJson(text);
1009
+ const type = typeof parsed.type === "string" ? parsed.type : "";
1010
+ if (type === "response.completed" || type === "response.done" || type === "response.incomplete") {
1011
+ state.sawCompletion = true;
1012
+ state.done = true;
1013
+ }
1014
+ state.queue.push(parsed);
1015
+ wakeWebSocketParser(state);
1016
+ }
1017
+ function processWebSocketError(event, state) {
1018
+ state.failed = extractWebSocketError(event);
1019
+ state.done = true;
1020
+ wakeWebSocketParser(state);
1021
+ }
1022
+ function processWebSocketClose(event, state) {
1023
+ if (state.sawCompletion) {
1024
+ state.done = true;
1025
+ wakeWebSocketParser(state);
1026
+ return;
1027
+ }
1028
+ if (!state.failed)
1029
+ state.failed = extractWebSocketCloseError(event);
1030
+ state.done = true;
1031
+ wakeWebSocketParser(state);
1032
+ }
1033
+ function abortWebSocketParsing(state) {
1034
+ state.failed = new Error("Request was aborted");
1035
+ state.done = true;
1036
+ wakeWebSocketParser(state);
1037
+ }
1038
+ async function waitForWebSocketActivity(socket, state, idleTimeoutMs) {
1039
+ let timeout;
1040
+ await new Promise((resolve, reject) => {
1041
+ state.pending = resolve;
1042
+ if (idleTimeoutMs !== undefined && idleTimeoutMs > 0) {
1043
+ timeout = setTimeout(() => {
1044
+ const error = new Error(`WebSocket idle timeout after ${idleTimeoutMs}ms`);
1045
+ state.failed = error;
1046
+ state.done = true;
1047
+ state.pending = null;
1048
+ closeWebSocketSilently(socket, 1000, "idle_timeout");
1049
+ reject(error);
1050
+ }, idleTimeoutMs);
1051
+ }
1052
+ }).finally(() => {
1053
+ if (timeout)
1054
+ clearTimeout(timeout);
1055
+ });
1056
+ }
1057
+ async function* parseWebSocket(socket, signal, idleTimeoutMs) {
1058
+ const state = {
1059
+ queue: [],
1060
+ pending: null,
1061
+ done: false,
1062
+ failed: null,
1063
+ sawCompletion: false,
1064
+ processing: Promise.resolve(),
909
1065
  };
910
- let processing = Promise.resolve();
911
1066
  const onMessage = (event) => {
912
- enqueue(async () => {
913
- if (!event || typeof event !== "object" || !("data" in event))
914
- return;
915
- const text = await decodeWebSocketData(event.data);
916
- if (!text)
917
- return;
918
- let parsed;
919
- try {
920
- parsed = JSON.parse(text);
921
- }
922
- catch (cause) {
923
- throw new CodexProtocolError(`Invalid Codex WebSocket JSON: ${formatThrownValue(cause)}`, {
924
- cause,
925
- payload: text,
926
- });
927
- }
928
- const type = typeof parsed.type === "string" ? parsed.type : "";
929
- if (type === "response.completed" || type === "response.done" || type === "response.incomplete") {
930
- sawCompletion = true;
931
- done = true;
932
- }
933
- queue.push(parsed);
934
- wake();
935
- });
1067
+ enqueueWebSocketEvent(state, () => processWebSocketMessage(event, state));
936
1068
  };
937
1069
  const onError = (event) => {
938
- enqueue(() => {
939
- failed = extractWebSocketError(event);
940
- done = true;
941
- wake();
942
- });
1070
+ enqueueWebSocketEvent(state, () => processWebSocketError(event, state));
943
1071
  };
944
1072
  const onClose = (event) => {
945
- enqueue(() => {
946
- if (sawCompletion) {
947
- done = true;
948
- wake();
949
- return;
950
- }
951
- if (!failed) {
952
- failed = extractWebSocketCloseError(event);
953
- }
954
- done = true;
955
- wake();
956
- });
957
- };
958
- const onAbort = () => {
959
- failed = new Error("Request was aborted");
960
- done = true;
961
- wake();
1073
+ enqueueWebSocketEvent(state, () => processWebSocketClose(event, state));
962
1074
  };
1075
+ const onAbort = () => abortWebSocketParsing(state);
963
1076
  socket.addEventListener("message", onMessage);
964
1077
  socket.addEventListener("error", onError);
965
1078
  socket.addEventListener("close", onClose);
966
1079
  signal?.addEventListener("abort", onAbort);
967
1080
  try {
968
1081
  while (true) {
969
- if (signal?.aborted) {
1082
+ if (signal?.aborted)
970
1083
  throw new Error("Request was aborted");
971
- }
972
- if (queue.length > 0) {
973
- yield queue.shift();
1084
+ if (state.queue.length > 0) {
1085
+ yield* state.queue.splice(0, 1);
974
1086
  continue;
975
1087
  }
976
- if (done)
1088
+ if (state.done)
977
1089
  break;
978
- let timeout;
979
- await new Promise((resolve, reject) => {
980
- pending = resolve;
981
- if (idleTimeoutMs !== undefined && idleTimeoutMs > 0) {
982
- timeout = setTimeout(() => {
983
- const error = new Error(`WebSocket idle timeout after ${idleTimeoutMs}ms`);
984
- failed = error;
985
- done = true;
986
- pending = null;
987
- closeWebSocketSilently(socket, 1000, "idle_timeout");
988
- reject(error);
989
- }, idleTimeoutMs);
990
- }
991
- }).finally(() => {
992
- if (timeout) {
993
- clearTimeout(timeout);
994
- }
995
- });
996
- }
997
- if (failed) {
998
- throw failed;
1090
+ await waitForWebSocketActivity(socket, state, idleTimeoutMs);
999
1091
  }
1000
- if (!sawCompletion) {
1092
+ if (state.failed)
1093
+ throw state.failed;
1094
+ if (!state.sawCompletion)
1001
1095
  throw new Error("WebSocket stream closed before response.completed");
1002
- }
1003
1096
  }
1004
1097
  finally {
1005
1098
  socket.removeEventListener("message", onMessage);
@@ -1060,6 +1153,44 @@ async function* startWebSocketOutputOnFirstEvent(events, output, stream, onStart
1060
1153
  yield event;
1061
1154
  }
1062
1155
  }
1156
+ function recordWebSocketRequestStats(stats, reused, useCachedContext, requestBody) {
1157
+ if (!stats)
1158
+ return;
1159
+ stats.requests++;
1160
+ if (reused)
1161
+ stats.connectionsReused++;
1162
+ else
1163
+ stats.connectionsCreated++;
1164
+ if (useCachedContext)
1165
+ stats.cachedContextRequests++;
1166
+ if (requestBody.store === true)
1167
+ stats.storeTrueRequests++;
1168
+ stats.lastInputItems = requestBody.input?.length ?? 0;
1169
+ if (requestBody.previous_response_id) {
1170
+ stats.deltaRequests++;
1171
+ stats.lastDeltaInputItems = requestBody.input?.length ?? 0;
1172
+ stats.lastPreviousResponseId = requestBody.previous_response_id;
1173
+ return;
1174
+ }
1175
+ stats.fullContextRequests++;
1176
+ stats.lastDeltaInputItems = undefined;
1177
+ stats.lastPreviousResponseId = undefined;
1178
+ }
1179
+ function finishWebSocketRequest(entry, fullBody, output, model, useCachedContext, signal) {
1180
+ if (signal?.aborted)
1181
+ return false;
1182
+ if (!useCachedContext || !entry || !output.responseId)
1183
+ return true;
1184
+ const responseItems = convertResponsesMessages(model, { messages: [output] }, CODEX_TOOL_CALL_PROVIDERS, {
1185
+ includeSystemPrompt: false,
1186
+ }).filter((item) => item.type !== "function_call_output");
1187
+ entry.continuation = {
1188
+ lastRequestBody: fullBody,
1189
+ lastResponseId: output.responseId,
1190
+ lastResponseItems: responseItems,
1191
+ };
1192
+ return true;
1193
+ }
1063
1194
  async function processWebSocketStream(url, body, headers, output, stream, model, onStart, idleTimeoutMs, websocketConnectTimeoutMs, options) {
1064
1195
  const { socket, entry, reused, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal, websocketConnectTimeoutMs);
1065
1196
  let keepConnection = true;
@@ -1069,28 +1200,7 @@ async function processWebSocketStream(url, body, headers, output, stream, model,
1069
1200
  const fullBody = body;
1070
1201
  const requestBody = useCachedContext && entry ? buildCachedWebSocketRequestBody(entry, fullBody) : fullBody;
1071
1202
  const stats = options?.sessionId ? getOrCreateWebSocketDebugStats(options.sessionId) : undefined;
1072
- if (stats) {
1073
- stats.requests++;
1074
- if (reused)
1075
- stats.connectionsReused++;
1076
- else
1077
- stats.connectionsCreated++;
1078
- if (useCachedContext)
1079
- stats.cachedContextRequests++;
1080
- if (requestBody.store === true)
1081
- stats.storeTrueRequests++;
1082
- stats.lastInputItems = requestBody.input?.length ?? 0;
1083
- if (requestBody.previous_response_id) {
1084
- stats.deltaRequests++;
1085
- stats.lastDeltaInputItems = requestBody.input?.length ?? 0;
1086
- stats.lastPreviousResponseId = requestBody.previous_response_id;
1087
- }
1088
- else {
1089
- stats.fullContextRequests++;
1090
- stats.lastDeltaInputItems = undefined;
1091
- stats.lastPreviousResponseId = undefined;
1092
- }
1093
- }
1203
+ recordWebSocketRequestStats(stats, reused, useCachedContext, requestBody);
1094
1204
  try {
1095
1205
  socket.send(JSON.stringify({ type: "response.create", ...requestBody }));
1096
1206
  await processResponsesStream(startWebSocketOutputOnFirstEvent(mapCodexEvents(parseWebSocket(socket, options?.signal, idleTimeoutMs)), output, stream, onStart), output, stream, model, {
@@ -1098,19 +1208,7 @@ async function processWebSocketStream(url, body, headers, output, stream, model,
1098
1208
  resolveServiceTier: resolveCodexServiceTier,
1099
1209
  applyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model),
1100
1210
  });
1101
- if (options?.signal?.aborted) {
1102
- keepConnection = false;
1103
- }
1104
- else if (useCachedContext && entry && output.responseId) {
1105
- const responseItems = convertResponsesMessages(model, { messages: [output] }, CODEX_TOOL_CALL_PROVIDERS, {
1106
- includeSystemPrompt: false,
1107
- }).filter((item) => item.type !== "function_call_output");
1108
- entry.continuation = {
1109
- lastRequestBody: fullBody,
1110
- lastResponseId: output.responseId,
1111
- lastResponseItems: responseItems,
1112
- };
1113
- }
1211
+ keepConnection = finishWebSocketRequest(entry, fullBody, output, model, useCachedContext, options?.signal);
1114
1212
  }
1115
1213
  catch (error) {
1116
1214
  if (entry) {
@@ -1126,28 +1224,29 @@ async function processWebSocketStream(url, body, headers, output, stream, model,
1126
1224
  // ============================================================================
1127
1225
  // Error Handling
1128
1226
  // ============================================================================
1227
+ function describeCodexApiError(error, status, fallbackMessage) {
1228
+ const code = error.code || error.type || "";
1229
+ if (!/usage_limit_reached|usage_not_included|rate_limit_exceeded/i.test(code) && status !== 429) {
1230
+ return { message: error.message || fallbackMessage };
1231
+ }
1232
+ const plan = error.plan_type ? ` (${error.plan_type.toLowerCase()} plan)` : "";
1233
+ const minutesUntilReset = error.resets_at
1234
+ ? Math.max(0, Math.round((error.resets_at * 1000 - Date.now()) / 60000))
1235
+ : undefined;
1236
+ const retryMessage = minutesUntilReset !== undefined ? ` Try again in ~${minutesUntilReset} min.` : "";
1237
+ const friendlyMessage = `You have hit your ChatGPT usage limit${plan}.${retryMessage}`.trim();
1238
+ return { message: error.message || friendlyMessage, friendlyMessage };
1239
+ }
1129
1240
  async function parseErrorResponse(response) {
1130
1241
  const raw = await response.text();
1131
- let message = raw || response.statusText || "Request failed";
1132
- let friendlyMessage;
1242
+ const fallbackMessage = raw || response.statusText || "Request failed";
1133
1243
  try {
1134
- const parsed = JSON.parse(raw);
1135
- const err = parsed?.error;
1136
- if (err) {
1137
- const code = err.code || err.type || "";
1138
- if (/usage_limit_reached|usage_not_included|rate_limit_exceeded/i.test(code) || response.status === 429) {
1139
- const plan = err.plan_type ? ` (${err.plan_type.toLowerCase()} plan)` : "";
1140
- const mins = err.resets_at
1141
- ? Math.max(0, Math.round((err.resets_at * 1000 - Date.now()) / 60000))
1142
- : undefined;
1143
- const when = mins !== undefined ? ` Try again in ~${mins} min.` : "";
1144
- friendlyMessage = `You have hit your ChatGPT usage limit${plan}.${when}`.trim();
1145
- }
1146
- message = err.message || friendlyMessage || message;
1147
- }
1244
+ const error = JSON.parse(raw).error;
1245
+ if (error)
1246
+ return describeCodexApiError(error, response.status, fallbackMessage);
1148
1247
  }
1149
1248
  catch { }
1150
- return { message, friendlyMessage };
1249
+ return { message: fallbackMessage };
1151
1250
  }
1152
1251
  // ============================================================================
1153
1252
  // Auth & Headers
@@ -1176,7 +1275,10 @@ function createCodexRequestId() {
1176
1275
  function buildBaseCodexHeaders(initHeaders, additionalHeaders, accountId, token) {
1177
1276
  const headers = new Headers(initHeaders);
1178
1277
  for (const [key, value] of Object.entries(additionalHeaders || {})) {
1179
- headers.set(key, value);
1278
+ if (value === null)
1279
+ headers.delete(key);
1280
+ else
1281
+ headers.set(key, value);
1180
1282
  }
1181
1283
  headers.set("Authorization", `Bearer ${token}`);
1182
1284
  headers.set("chatgpt-account-id", accountId);