@draht/ai 2026.4.26 → 2026.6.11

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 (198) hide show
  1. package/dist/api-registry.d.ts +1 -1
  2. package/dist/api-registry.d.ts.map +1 -1
  3. package/dist/api-registry.js.map +1 -1
  4. package/dist/bedrock-provider.d.ts +2 -2
  5. package/dist/bedrock-provider.d.ts.map +1 -1
  6. package/dist/bedrock-provider.js.map +1 -1
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +14 -0
  9. package/dist/cli.js.map +1 -1
  10. package/dist/env-api-keys.d.ts +10 -1
  11. package/dist/env-api-keys.d.ts.map +1 -1
  12. package/dist/env-api-keys.js +110 -36
  13. package/dist/env-api-keys.js.map +1 -1
  14. package/dist/image-models.d.ts +10 -0
  15. package/dist/image-models.d.ts.map +1 -0
  16. package/dist/image-models.generated.d.ts +485 -0
  17. package/dist/image-models.generated.d.ts.map +1 -0
  18. package/dist/image-models.generated.js +487 -0
  19. package/dist/image-models.generated.js.map +1 -0
  20. package/dist/image-models.js +23 -0
  21. package/dist/image-models.js.map +1 -0
  22. package/dist/images-api-registry.d.ts +14 -0
  23. package/dist/images-api-registry.d.ts.map +1 -0
  24. package/dist/images-api-registry.js +22 -0
  25. package/dist/images-api-registry.js.map +1 -0
  26. package/dist/images.d.ts +4 -0
  27. package/dist/images.d.ts.map +1 -0
  28. package/dist/images.js +14 -0
  29. package/dist/images.js.map +1 -0
  30. package/dist/index.d.ts +31 -25
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +7 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/models.d.ts +5 -8
  35. package/dist/models.d.ts.map +1 -1
  36. package/dist/models.generated.d.ts +5197 -1721
  37. package/dist/models.generated.d.ts.map +1 -1
  38. package/dist/models.generated.js +7156 -5016
  39. package/dist/models.generated.js.map +1 -1
  40. package/dist/models.js +33 -6
  41. package/dist/models.js.map +1 -1
  42. package/dist/oauth.d.ts +1 -1
  43. package/dist/oauth.d.ts.map +1 -1
  44. package/dist/oauth.js.map +1 -1
  45. package/dist/providers/amazon-bedrock.d.ts +19 -1
  46. package/dist/providers/amazon-bedrock.d.ts.map +1 -1
  47. package/dist/providers/amazon-bedrock.js +278 -89
  48. package/dist/providers/amazon-bedrock.js.map +1 -1
  49. package/dist/providers/anthropic.d.ts +37 -6
  50. package/dist/providers/anthropic.d.ts.map +1 -1
  51. package/dist/providers/anthropic.js +300 -114
  52. package/dist/providers/anthropic.js.map +1 -1
  53. package/dist/providers/azure-openai-responses.d.ts +1 -1
  54. package/dist/providers/azure-openai-responses.d.ts.map +1 -1
  55. package/dist/providers/azure-openai-responses.js +68 -21
  56. package/dist/providers/azure-openai-responses.js.map +1 -1
  57. package/dist/providers/cloudflare.d.ts +13 -0
  58. package/dist/providers/cloudflare.d.ts.map +1 -0
  59. package/dist/providers/cloudflare.js +26 -0
  60. package/dist/providers/cloudflare.js.map +1 -0
  61. package/dist/providers/faux.d.ts +1 -1
  62. package/dist/providers/faux.d.ts.map +1 -1
  63. package/dist/providers/faux.js +1 -0
  64. package/dist/providers/faux.js.map +1 -1
  65. package/dist/providers/github-copilot-headers.d.ts +1 -1
  66. package/dist/providers/github-copilot-headers.d.ts.map +1 -1
  67. package/dist/providers/github-copilot-headers.js.map +1 -1
  68. package/dist/providers/google-shared.d.ts +8 -3
  69. package/dist/providers/google-shared.d.ts.map +1 -1
  70. package/dist/providers/google-shared.js +34 -17
  71. package/dist/providers/google-shared.js.map +1 -1
  72. package/dist/providers/google-vertex.d.ts +2 -2
  73. package/dist/providers/google-vertex.d.ts.map +1 -1
  74. package/dist/providers/google-vertex.js +45 -18
  75. package/dist/providers/google-vertex.js.map +1 -1
  76. package/dist/providers/google.d.ts +2 -2
  77. package/dist/providers/google.d.ts.map +1 -1
  78. package/dist/providers/google.js +9 -6
  79. package/dist/providers/google.js.map +1 -1
  80. package/dist/providers/images/openrouter.d.ts +3 -0
  81. package/dist/providers/images/openrouter.d.ts.map +1 -0
  82. package/dist/providers/images/openrouter.js +128 -0
  83. package/dist/providers/images/openrouter.js.map +1 -0
  84. package/dist/providers/images/register-builtins.d.ts +4 -0
  85. package/dist/providers/images/register-builtins.d.ts.map +1 -0
  86. package/dist/providers/images/register-builtins.js +34 -0
  87. package/dist/providers/images/register-builtins.js.map +1 -0
  88. package/dist/providers/mistral.d.ts +4 -1
  89. package/dist/providers/mistral.d.ts.map +1 -1
  90. package/dist/providers/mistral.js +43 -10
  91. package/dist/providers/mistral.js.map +1 -1
  92. package/dist/providers/openai-codex-responses.d.ts +22 -1
  93. package/dist/providers/openai-codex-responses.d.ts.map +1 -1
  94. package/dist/providers/openai-codex-responses.js +542 -111
  95. package/dist/providers/openai-codex-responses.js.map +1 -1
  96. package/dist/providers/openai-completions.d.ts +6 -2
  97. package/dist/providers/openai-completions.d.ts.map +1 -1
  98. package/dist/providers/openai-completions.js +446 -227
  99. package/dist/providers/openai-completions.js.map +1 -1
  100. package/dist/providers/openai-prompt-cache.d.ts +3 -0
  101. package/dist/providers/openai-prompt-cache.d.ts.map +1 -0
  102. package/dist/providers/openai-prompt-cache.js +10 -0
  103. package/dist/providers/openai-prompt-cache.js.map +1 -0
  104. package/dist/providers/openai-responses-shared.d.ts +3 -2
  105. package/dist/providers/openai-responses-shared.d.ts.map +1 -1
  106. package/dist/providers/openai-responses-shared.js +41 -15
  107. package/dist/providers/openai-responses-shared.js.map +1 -1
  108. package/dist/providers/openai-responses.d.ts +1 -1
  109. package/dist/providers/openai-responses.d.ts.map +1 -1
  110. package/dist/providers/openai-responses.js +85 -40
  111. package/dist/providers/openai-responses.js.map +1 -1
  112. package/dist/providers/register-builtins.d.ts +10 -13
  113. package/dist/providers/register-builtins.d.ts.map +1 -1
  114. package/dist/providers/register-builtins.js +13 -20
  115. package/dist/providers/register-builtins.js.map +1 -1
  116. package/dist/providers/simple-options.d.ts +2 -2
  117. package/dist/providers/simple-options.d.ts.map +1 -1
  118. package/dist/providers/simple-options.js +8 -2
  119. package/dist/providers/simple-options.js.map +1 -1
  120. package/dist/providers/transform-messages.d.ts +1 -1
  121. package/dist/providers/transform-messages.d.ts.map +1 -1
  122. package/dist/providers/transform-messages.js +63 -34
  123. package/dist/providers/transform-messages.js.map +1 -1
  124. package/dist/session-resources.d.ts +4 -0
  125. package/dist/session-resources.d.ts.map +1 -0
  126. package/dist/session-resources.js +22 -0
  127. package/dist/session-resources.js.map +1 -0
  128. package/dist/stream.d.ts +3 -3
  129. package/dist/stream.d.ts.map +1 -1
  130. package/dist/stream.js +14 -2
  131. package/dist/stream.js.map +1 -1
  132. package/dist/types.d.ts +177 -14
  133. package/dist/types.d.ts.map +1 -1
  134. package/dist/types.js.map +1 -1
  135. package/dist/utils/abort-signals.d.ts +6 -0
  136. package/dist/utils/abort-signals.d.ts.map +1 -0
  137. package/dist/utils/abort-signals.js +34 -0
  138. package/dist/utils/abort-signals.js.map +1 -0
  139. package/dist/utils/diagnostics.d.ts +19 -0
  140. package/dist/utils/diagnostics.d.ts.map +1 -0
  141. package/dist/utils/diagnostics.js +25 -0
  142. package/dist/utils/diagnostics.js.map +1 -0
  143. package/dist/utils/event-stream.d.ts +3 -3
  144. package/dist/utils/event-stream.d.ts.map +1 -1
  145. package/dist/utils/event-stream.js +2 -2
  146. package/dist/utils/event-stream.js.map +1 -1
  147. package/dist/utils/headers.d.ts +2 -0
  148. package/dist/utils/headers.d.ts.map +1 -0
  149. package/dist/utils/headers.js +8 -0
  150. package/dist/utils/headers.js.map +1 -0
  151. package/dist/utils/json-parse.d.ts +8 -1
  152. package/dist/utils/json-parse.d.ts.map +1 -1
  153. package/dist/utils/json-parse.js +89 -5
  154. package/dist/utils/json-parse.js.map +1 -1
  155. package/dist/utils/node-http-proxy.d.ts +10 -0
  156. package/dist/utils/node-http-proxy.d.ts.map +1 -0
  157. package/dist/utils/node-http-proxy.js +97 -0
  158. package/dist/utils/node-http-proxy.js.map +1 -0
  159. package/dist/utils/oauth/anthropic.d.ts +1 -1
  160. package/dist/utils/oauth/anthropic.d.ts.map +1 -1
  161. package/dist/utils/oauth/anthropic.js +1 -1
  162. package/dist/utils/oauth/anthropic.js.map +1 -1
  163. package/dist/utils/oauth/device-code.d.ts +21 -0
  164. package/dist/utils/oauth/device-code.d.ts.map +1 -0
  165. package/dist/utils/oauth/device-code.js +56 -0
  166. package/dist/utils/oauth/device-code.js.map +1 -0
  167. package/dist/utils/oauth/github-copilot.d.ts +3 -3
  168. package/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  169. package/dist/utils/oauth/github-copilot.js +58 -70
  170. package/dist/utils/oauth/github-copilot.js.map +1 -1
  171. package/dist/utils/oauth/index.d.ts +8 -11
  172. package/dist/utils/oauth/index.d.ts.map +1 -1
  173. package/dist/utils/oauth/index.js +2 -11
  174. package/dist/utils/oauth/index.js.map +1 -1
  175. package/dist/utils/oauth/openai-codex.d.ts +11 -2
  176. package/dist/utils/oauth/openai-codex.d.ts.map +1 -1
  177. package/dist/utils/oauth/openai-codex.js +187 -73
  178. package/dist/utils/oauth/openai-codex.js.map +1 -1
  179. package/dist/utils/oauth/types.d.ts +18 -1
  180. package/dist/utils/oauth/types.d.ts.map +1 -1
  181. package/dist/utils/oauth/types.js.map +1 -1
  182. package/dist/utils/overflow.d.ts +7 -3
  183. package/dist/utils/overflow.d.ts.map +1 -1
  184. package/dist/utils/overflow.js +25 -3
  185. package/dist/utils/overflow.js.map +1 -1
  186. package/dist/utils/typebox-helpers.d.ts +1 -1
  187. package/dist/utils/typebox-helpers.d.ts.map +1 -1
  188. package/dist/utils/typebox-helpers.js +1 -1
  189. package/dist/utils/typebox-helpers.js.map +1 -1
  190. package/dist/utils/validation.d.ts +1 -1
  191. package/dist/utils/validation.d.ts.map +1 -1
  192. package/dist/utils/validation.js +242 -41
  193. package/dist/utils/validation.js.map +1 -1
  194. package/package.json +15 -16
  195. package/dist/providers/google-gemini-cli.d.ts +0 -74
  196. package/dist/providers/google-gemini-cli.d.ts.map +0 -1
  197. package/dist/providers/google-gemini-cli.js +0 -776
  198. package/dist/providers/google-gemini-cli.js.map +0 -1
@@ -1,25 +1,41 @@
1
- // NEVER convert to top-level runtime imports - breaks browser/Vite builds (web-ui)
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ // NEVER convert to top-level runtime imports - breaks browser/Vite builds
2
10
  let _os = null;
3
- const dynamicImport = (specifier) => import(specifier);
11
+ const dynamicImport = (specifier) => import(__rewriteRelativeImportExtension(specifier));
4
12
  const NODE_OS_SPECIFIER = "node:" + "os";
5
13
  if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
6
14
  dynamicImport(NODE_OS_SPECIFIER).then((m) => {
7
15
  _os = m;
8
16
  });
9
17
  }
10
- import { getEnvApiKey } from "../env-api-keys.js";
11
- import { supportsXhigh } from "../models.js";
18
+ import { clampThinkingLevel } from "../models.js";
19
+ import { registerSessionResourceCleanup } from "../session-resources.js";
20
+ import { combineAbortSignals } from "../utils/abort-signals.js";
21
+ import { appendAssistantMessageDiagnostic, createAssistantMessageDiagnostic, formatThrownValue, } from "../utils/diagnostics.js";
12
22
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
23
+ import { headersToRecord } from "../utils/headers.js";
24
+ import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
13
25
  import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js";
14
- import { buildBaseOptions, clampReasoning, clampToXhigh } from "./simple-options.js";
26
+ import { buildBaseOptions, clampToXhigh } from "./simple-options.js";
15
27
  // ============================================================================
16
28
  // Configuration
17
29
  // ============================================================================
18
30
  const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
19
31
  const JWT_CLAIM_PATH = "https://api.openai.com/auth";
20
- const MAX_RETRIES = 3;
32
+ const DEFAULT_MAX_RETRIES = 0;
21
33
  const BASE_DELAY_MS = 1000;
34
+ const DEFAULT_MAX_RETRY_DELAY_MS = 60_000;
35
+ const DEFAULT_SSE_HEADER_TIMEOUT_MS = 10_000;
36
+ const DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS = 15_000;
22
37
  const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
38
+ const WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009;
23
39
  const CODEX_RESPONSE_STATUSES = new Set([
24
40
  "completed",
25
41
  "incomplete",
@@ -31,12 +47,44 @@ const CODEX_RESPONSE_STATUSES = new Set([
31
47
  // ============================================================================
32
48
  // Retry Helpers
33
49
  // ============================================================================
50
+ function isTerminalRateLimitError(errorText) {
51
+ return /GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(errorText);
52
+ }
34
53
  function isRetryableError(status, errorText) {
54
+ if (status === 429 && isTerminalRateLimitError(errorText)) {
55
+ return false;
56
+ }
35
57
  if (status === 429 || status === 500 || status === 502 || status === 503 || status === 504) {
36
58
  return true;
37
59
  }
38
60
  return /rate.?limit|overloaded|service.?unavailable|upstream.?connect|connection.?refused/i.test(errorText);
39
61
  }
62
+ function getRetryAfterDelayMs(headers) {
63
+ const retryAfterMs = headers.get("retry-after-ms");
64
+ if (retryAfterMs !== null) {
65
+ const millis = Number(retryAfterMs);
66
+ if (Number.isFinite(millis)) {
67
+ return Math.max(0, millis);
68
+ }
69
+ }
70
+ const retryAfter = headers.get("retry-after");
71
+ if (!retryAfter) {
72
+ return undefined;
73
+ }
74
+ const seconds = Number(retryAfter);
75
+ if (Number.isFinite(seconds)) {
76
+ return Math.max(0, seconds * 1000);
77
+ }
78
+ const date = Date.parse(retryAfter);
79
+ if (!Number.isNaN(date)) {
80
+ return Math.max(0, date - Date.now());
81
+ }
82
+ return undefined;
83
+ }
84
+ function capRetryDelayMs(delayMs, options) {
85
+ const maxRetryDelayMs = options?.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
86
+ return maxRetryDelayMs > 0 ? Math.min(delayMs, maxRetryDelayMs) : delayMs;
87
+ }
40
88
  function sleep(ms, signal) {
41
89
  return new Promise((resolve, reject) => {
42
90
  if (signal?.aborted) {
@@ -50,6 +98,27 @@ function sleep(ms, signal) {
50
98
  });
51
99
  });
52
100
  }
101
+ function normalizeTimeoutMs(value) {
102
+ if (value === undefined)
103
+ return undefined;
104
+ if (!Number.isFinite(value) || value < 0) {
105
+ throw new Error(`Invalid timeoutMs: ${String(value)}`);
106
+ }
107
+ return Math.floor(value);
108
+ }
109
+ function createSSEHeaderTimeout() {
110
+ const controller = new AbortController();
111
+ let error;
112
+ const timeout = setTimeout(() => {
113
+ error = new Error(`Codex SSE response headers timed out after ${DEFAULT_SSE_HEADER_TIMEOUT_MS}ms`);
114
+ controller.abort(error);
115
+ }, DEFAULT_SSE_HEADER_TIMEOUT_MS);
116
+ return {
117
+ signal: controller.signal,
118
+ clear: () => clearTimeout(timeout),
119
+ error: () => error,
120
+ };
121
+ }
53
122
  // ============================================================================
54
123
  // Main Stream Function
55
124
  // ============================================================================
@@ -74,7 +143,7 @@ export const streamOpenAICodexResponses = (model, context, options) => {
74
143
  timestamp: Date.now(),
75
144
  };
76
145
  try {
77
- const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
146
+ const apiKey = options?.apiKey;
78
147
  if (!apiKey) {
79
148
  throw new Error(`No API key for provider: ${model.provider}`);
80
149
  }
@@ -88,13 +157,19 @@ export const streamOpenAICodexResponses = (model, context, options) => {
88
157
  const sseHeaders = buildSSEHeaders(model.headers, options?.headers, accountId, apiKey, options?.sessionId);
89
158
  const websocketHeaders = buildWebSocketHeaders(model.headers, options?.headers, accountId, apiKey, websocketRequestId);
90
159
  const bodyJson = JSON.stringify(body);
91
- const transport = options?.transport || "sse";
92
- if (transport !== "sse") {
160
+ const idleTimeoutMs = normalizeTimeoutMs(options?.timeoutMs);
161
+ const websocketConnectTimeoutMs = normalizeTimeoutMs(options?.websocketConnectTimeoutMs);
162
+ const transport = options?.transport || "auto";
163
+ const websocketDisabledForSession = transport !== "sse" && isWebSocketSseFallbackActive(options?.sessionId);
164
+ if (websocketDisabledForSession) {
165
+ recordWebSocketSseFallback(options?.sessionId);
166
+ }
167
+ if (transport !== "sse" && !websocketDisabledForSession) {
93
168
  let websocketStarted = false;
94
169
  try {
95
170
  await processWebSocketStream(resolveCodexWebSocketUrl(model.baseUrl), body, websocketHeaders, output, stream, model, () => {
96
171
  websocketStarted = true;
97
- }, options);
172
+ }, idleTimeoutMs, websocketConnectTimeoutMs, options);
98
173
  if (options?.signal?.aborted) {
99
174
  throw new Error("Request was aborted");
100
175
  }
@@ -107,31 +182,63 @@ export const streamOpenAICodexResponses = (model, context, options) => {
107
182
  return;
108
183
  }
109
184
  catch (error) {
110
- if (transport === "websocket" || websocketStarted) {
185
+ const aborted = options?.signal?.aborted;
186
+ if (aborted || isCodexNonTransportError(error)) {
111
187
  throw error;
112
188
  }
189
+ appendAssistantMessageDiagnostic(output, createAssistantMessageDiagnostic("provider_transport_failure", error, {
190
+ configuredTransport: transport,
191
+ fallbackTransport: websocketStarted ? undefined : "sse",
192
+ eventsEmitted: websocketStarted,
193
+ phase: websocketStarted ? "after_message_stream_start" : "before_message_stream_start",
194
+ requestBytes: new TextEncoder().encode(bodyJson).byteLength,
195
+ }));
196
+ recordWebSocketFailure(options?.sessionId, error);
197
+ if (websocketStarted) {
198
+ throw error;
199
+ }
200
+ recordWebSocketSseFallback(options?.sessionId);
113
201
  }
114
202
  }
115
203
  // Fetch with retry logic for rate limits and transient errors
116
204
  let response;
117
205
  let lastError;
118
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
206
+ const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
207
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
119
208
  if (options?.signal?.aborted) {
120
209
  throw new Error("Request was aborted");
121
210
  }
122
211
  try {
123
- response = await fetch(resolveCodexUrl(model.baseUrl), {
124
- method: "POST",
125
- headers: sseHeaders,
126
- body: bodyJson,
127
- signal: options?.signal,
128
- });
212
+ const headerTimeout = createSSEHeaderTimeout();
213
+ const combinedSignal = combineAbortSignals([options?.signal, headerTimeout.signal]);
214
+ try {
215
+ response = await fetch(resolveCodexUrl(model.baseUrl), {
216
+ method: "POST",
217
+ headers: sseHeaders,
218
+ body: bodyJson,
219
+ signal: combinedSignal.signal,
220
+ });
221
+ }
222
+ catch (error) {
223
+ const timeoutError = headerTimeout.error();
224
+ throw timeoutError && !options?.signal?.aborted ? timeoutError : error;
225
+ }
226
+ finally {
227
+ combinedSignal.cleanup();
228
+ headerTimeout.clear();
229
+ }
230
+ await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
129
231
  if (response.ok) {
130
232
  break;
131
233
  }
132
234
  const errorText = await response.text();
133
- if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
134
- const delayMs = BASE_DELAY_MS * 2 ** attempt;
235
+ if (attempt < maxRetries && isRetryableError(response.status, errorText)) {
236
+ const retryAfterDelayMs = getRetryAfterDelayMs(response.headers);
237
+ const delayMs = retryAfterDelayMs === undefined
238
+ ? BASE_DELAY_MS * 2 ** attempt
239
+ : response.status === 429
240
+ ? capRetryDelayMs(retryAfterDelayMs, options)
241
+ : retryAfterDelayMs;
135
242
  await sleep(delayMs, options?.signal);
136
243
  continue;
137
244
  }
@@ -151,7 +258,7 @@ export const streamOpenAICodexResponses = (model, context, options) => {
151
258
  }
152
259
  lastError = error instanceof Error ? error : new Error(String(error));
153
260
  // Network errors are retryable
154
- if (attempt < MAX_RETRIES && !lastError.message.includes("usage limit")) {
261
+ if (attempt < maxRetries && !lastError.message.includes("usage limit")) {
155
262
  const delayMs = BASE_DELAY_MS * 2 ** attempt;
156
263
  await sleep(delayMs, options?.signal);
157
264
  continue;
@@ -166,7 +273,7 @@ export const streamOpenAICodexResponses = (model, context, options) => {
166
273
  throw new Error("No response body");
167
274
  }
168
275
  stream.push({ type: "start", partial: output });
169
- await processStream(response, output, stream, model);
276
+ await processStream(response, output, stream, model, options);
170
277
  if (options?.signal?.aborted) {
171
278
  throw new Error("Request was aborted");
172
279
  }
@@ -174,6 +281,10 @@ export const streamOpenAICodexResponses = (model, context, options) => {
174
281
  stream.end();
175
282
  }
176
283
  catch (error) {
284
+ for (const block of output.content) {
285
+ // partialJson is only a streaming scratch buffer; never persist it.
286
+ delete block.partialJson;
287
+ }
177
288
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
178
289
  output.errorMessage = error instanceof Error ? error.message : String(error);
179
290
  stream.push({ type: "error", reason: output.stopReason, error: output });
@@ -183,12 +294,14 @@ export const streamOpenAICodexResponses = (model, context, options) => {
183
294
  return stream;
184
295
  };
185
296
  export const streamSimpleOpenAICodexResponses = (model, context, options) => {
186
- const apiKey = options?.apiKey || getEnvApiKey(model.provider);
297
+ const apiKey = options?.apiKey;
187
298
  if (!apiKey) {
188
299
  throw new Error(`No API key for provider: ${model.provider}`);
189
300
  }
190
301
  const base = buildBaseOptions(model, options, apiKey);
191
- const reasoningEffort = supportsXhigh(model) ? clampToXhigh(options?.reasoning) : clampReasoning(options?.reasoning);
302
+ const rawLevel = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
303
+ const clampedReasoning = rawLevel === "off" ? rawLevel : clampToXhigh(rawLevel);
304
+ const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
192
305
  return streamOpenAICodexResponses(model, context, {
193
306
  ...base,
194
307
  reasoningEffort,
@@ -205,37 +318,61 @@ function buildRequestBody(model, context, options) {
205
318
  model: model.id,
206
319
  store: false,
207
320
  stream: true,
208
- instructions: context.systemPrompt,
321
+ instructions: context.systemPrompt || "You are a helpful assistant.",
209
322
  input: messages,
210
- text: { verbosity: options?.textVerbosity || "medium" },
323
+ text: { verbosity: options?.textVerbosity || "low" },
211
324
  include: ["reasoning.encrypted_content"],
212
- prompt_cache_key: options?.sessionId,
325
+ prompt_cache_key: clampOpenAIPromptCacheKey(options?.sessionId),
213
326
  tool_choice: "auto",
214
327
  parallel_tool_calls: true,
215
328
  };
216
329
  if (options?.temperature !== undefined) {
217
330
  body.temperature = options.temperature;
218
331
  }
219
- if (context.tools) {
332
+ if (options?.serviceTier !== undefined) {
333
+ body.service_tier = options.serviceTier;
334
+ }
335
+ if (context.tools && context.tools.length > 0) {
220
336
  body.tools = convertResponsesTools(context.tools, { strict: null });
221
337
  }
222
338
  if (options?.reasoningEffort !== undefined) {
223
- body.reasoning = {
224
- effort: clampReasoningEffort(model.id, options.reasoningEffort),
225
- summary: options.reasoningSummary ?? "auto",
226
- };
339
+ const effort = options.reasoningEffort === "none"
340
+ ? (model.thinkingLevelMap?.off ?? "none")
341
+ : (model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort);
342
+ if (effort !== null) {
343
+ body.reasoning = {
344
+ effort,
345
+ summary: options.reasoningSummary ?? "auto",
346
+ };
347
+ }
227
348
  }
228
349
  return body;
229
350
  }
230
- function clampReasoningEffort(modelId, effort) {
231
- const id = modelId.includes("/") ? modelId.split("/").pop() : modelId;
232
- if ((id.startsWith("gpt-5.2") || id.startsWith("gpt-5.3") || id.startsWith("gpt-5.4")) && effort === "minimal")
233
- return "low";
234
- if (id === "gpt-5.1" && effort === "xhigh")
235
- return "high";
236
- if (id === "gpt-5.1-codex-mini")
237
- return effort === "high" || effort === "xhigh" ? "high" : "medium";
238
- return effort;
351
+ function getServiceTierCostMultiplier(model, serviceTier) {
352
+ switch (serviceTier) {
353
+ case "flex":
354
+ return 0.5;
355
+ case "priority":
356
+ return model.id === "gpt-5.5" ? 2.5 : 2;
357
+ default:
358
+ return 1;
359
+ }
360
+ }
361
+ function applyServiceTierPricing(usage, serviceTier, model) {
362
+ const multiplier = getServiceTierCostMultiplier(model, serviceTier);
363
+ if (multiplier === 1)
364
+ return;
365
+ usage.cost.input *= multiplier;
366
+ usage.cost.output *= multiplier;
367
+ usage.cost.cacheRead *= multiplier;
368
+ usage.cost.cacheWrite *= multiplier;
369
+ usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
370
+ }
371
+ function resolveCodexServiceTier(responseServiceTier, requestServiceTier) {
372
+ if (responseServiceTier === "default" && (requestServiceTier === "flex" || requestServiceTier === "priority")) {
373
+ return requestServiceTier;
374
+ }
375
+ return responseServiceTier ?? requestServiceTier;
239
376
  }
240
377
  function resolveCodexUrl(baseUrl) {
241
378
  const raw = baseUrl && baseUrl.trim().length > 0 ? baseUrl : DEFAULT_CODEX_BASE_URL;
@@ -257,8 +394,35 @@ function resolveCodexWebSocketUrl(baseUrl) {
257
394
  // ============================================================================
258
395
  // Response Processing
259
396
  // ============================================================================
260
- async function processStream(response, output, stream, model) {
261
- await processResponsesStream(mapCodexEvents(parseSSE(response)), output, stream, model);
397
+ async function processStream(response, output, stream, model, options) {
398
+ await processResponsesStream(mapCodexEvents(parseSSE(response, options?.signal)), output, stream, model, {
399
+ serviceTier: options?.serviceTier,
400
+ resolveServiceTier: resolveCodexServiceTier,
401
+ applyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model),
402
+ });
403
+ }
404
+ class CodexApiError extends Error {
405
+ code;
406
+ payload;
407
+ constructor(message, options) {
408
+ super(message);
409
+ this.name = "CodexApiError";
410
+ this.code = options?.code;
411
+ this.payload = options?.payload;
412
+ this.cause = options?.cause;
413
+ }
414
+ }
415
+ class CodexProtocolError extends Error {
416
+ payload;
417
+ constructor(message, options) {
418
+ super(message);
419
+ this.name = "CodexProtocolError";
420
+ this.payload = options?.payload;
421
+ this.cause = options?.cause;
422
+ }
423
+ }
424
+ function isCodexNonTransportError(error) {
425
+ return error instanceof CodexApiError || error instanceof CodexProtocolError;
262
426
  }
263
427
  async function* mapCodexEvents(events) {
264
428
  for await (const event of events) {
@@ -268,11 +432,16 @@ async function* mapCodexEvents(events) {
268
432
  if (type === "error") {
269
433
  const code = event.code || "";
270
434
  const message = event.message || "";
271
- throw new Error(`Codex error: ${message || code || JSON.stringify(event)}`);
435
+ throw new CodexApiError(`Codex error: ${message || code || JSON.stringify(event)}`, {
436
+ code: code || undefined,
437
+ payload: event,
438
+ });
272
439
  }
273
440
  if (type === "response.failed") {
274
- const msg = event.response?.error?.message;
275
- throw new Error(msg || "Codex response failed");
441
+ const response = event.response;
442
+ const code = response?.error?.code;
443
+ const message = response?.error?.message;
444
+ throw new CodexApiError(message || "Codex response failed", { code, payload: event });
276
445
  }
277
446
  if (type === "response.done" || type === "response.completed" || type === "response.incomplete") {
278
447
  const response = event.response;
@@ -293,15 +462,25 @@ function normalizeCodexStatus(status) {
293
462
  // ============================================================================
294
463
  // SSE Parsing
295
464
  // ============================================================================
296
- async function* parseSSE(response) {
465
+ async function* parseSSE(response, signal) {
297
466
  if (!response.body)
298
467
  return;
299
468
  const reader = response.body.getReader();
300
469
  const decoder = new TextDecoder();
301
470
  let buffer = "";
471
+ const onAbort = () => {
472
+ void reader.cancel().catch(() => { });
473
+ };
474
+ signal?.addEventListener("abort", onAbort, { once: true });
302
475
  try {
303
476
  while (true) {
477
+ if (signal?.aborted) {
478
+ throw new Error("Request was aborted");
479
+ }
304
480
  const { done, value } = await reader.read();
481
+ if (signal?.aborted) {
482
+ throw new Error("Request was aborted");
483
+ }
305
484
  if (done)
306
485
  break;
307
486
  buffer += decoder.decode(value, { stream: true });
@@ -319,7 +498,12 @@ async function* parseSSE(response) {
319
498
  try {
320
499
  yield JSON.parse(data);
321
500
  }
322
- catch { }
501
+ catch (cause) {
502
+ throw new CodexProtocolError(`Invalid Codex SSE JSON: ${formatThrownValue(cause)}`, {
503
+ cause,
504
+ payload: data,
505
+ });
506
+ }
323
507
  }
324
508
  }
325
509
  idx = buffer.indexOf("\n\n");
@@ -327,6 +511,7 @@ async function* parseSSE(response) {
327
511
  }
328
512
  }
329
513
  finally {
514
+ signal?.removeEventListener("abort", onAbort);
330
515
  try {
331
516
  await reader.cancel();
332
517
  }
@@ -343,18 +528,119 @@ async function* parseSSE(response) {
343
528
  const OPENAI_BETA_RESPONSES_WEBSOCKETS = "responses_websockets=2026-02-06";
344
529
  const SESSION_WEBSOCKET_CACHE_TTL_MS = 5 * 60 * 1000;
345
530
  const websocketSessionCache = new Map();
346
- function getWebSocketConstructor() {
531
+ const websocketDebugStats = new Map();
532
+ const websocketSseFallbackSessions = new Set();
533
+ function getOrCreateWebSocketDebugStats(sessionId) {
534
+ let stats = websocketDebugStats.get(sessionId);
535
+ if (!stats) {
536
+ stats = {
537
+ requests: 0,
538
+ connectionsCreated: 0,
539
+ connectionsReused: 0,
540
+ cachedContextRequests: 0,
541
+ storeTrueRequests: 0,
542
+ fullContextRequests: 0,
543
+ deltaRequests: 0,
544
+ lastInputItems: 0,
545
+ websocketFailures: 0,
546
+ sseFallbacks: 0,
547
+ };
548
+ websocketDebugStats.set(sessionId, stats);
549
+ }
550
+ return stats;
551
+ }
552
+ export function getOpenAICodexWebSocketDebugStats(sessionId) {
553
+ const stats = websocketDebugStats.get(sessionId);
554
+ return stats ? { ...stats } : undefined;
555
+ }
556
+ export function resetOpenAICodexWebSocketDebugStats(sessionId) {
557
+ if (sessionId) {
558
+ websocketDebugStats.delete(sessionId);
559
+ websocketSseFallbackSessions.delete(sessionId);
560
+ return;
561
+ }
562
+ websocketDebugStats.clear();
563
+ websocketSseFallbackSessions.clear();
564
+ }
565
+ export function closeOpenAICodexWebSocketSessions(sessionId) {
566
+ const closeEntry = (entry) => {
567
+ if (entry.idleTimer)
568
+ clearTimeout(entry.idleTimer);
569
+ closeWebSocketSilently(entry.socket, 1000, "debug_close");
570
+ };
571
+ if (sessionId) {
572
+ const entry = websocketSessionCache.get(sessionId);
573
+ if (entry)
574
+ closeEntry(entry);
575
+ websocketSessionCache.delete(sessionId);
576
+ return;
577
+ }
578
+ for (const entry of websocketSessionCache.values()) {
579
+ closeEntry(entry);
580
+ }
581
+ websocketSessionCache.clear();
582
+ }
583
+ registerSessionResourceCleanup(closeOpenAICodexWebSocketSessions);
584
+ function isWebSocketSseFallbackActive(sessionId) {
585
+ return sessionId ? websocketSseFallbackSessions.has(sessionId) : false;
586
+ }
587
+ function recordWebSocketSseFallback(sessionId) {
588
+ if (!sessionId)
589
+ return;
590
+ const stats = getOrCreateWebSocketDebugStats(sessionId);
591
+ stats.sseFallbacks++;
592
+ stats.websocketFallbackActive = isWebSocketSseFallbackActive(sessionId);
593
+ }
594
+ function recordWebSocketFailure(sessionId, error) {
595
+ if (!sessionId)
596
+ return;
597
+ websocketSseFallbackSessions.add(sessionId);
598
+ const stats = getOrCreateWebSocketDebugStats(sessionId);
599
+ stats.websocketFailures++;
600
+ stats.lastWebSocketError = formatThrownValue(error);
601
+ stats.websocketFallbackActive = true;
602
+ }
603
+ let _cachedWebsocket = null;
604
+ async function getWebSocketConstructor() {
605
+ if (_cachedWebsocket)
606
+ return _cachedWebsocket;
607
+ // bun doesn't respect http proxy envs, ref: https://github.com/oven-sh/bun/issues/15489
608
+ // TODO: remove this when bun supports proxy envs in websocket.
609
+ if (process?.versions?.bun &&
610
+ (process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy)) {
611
+ const m = await dynamicImport("proxy-from-env");
612
+ const getProxyForUrl = m.getProxyForUrl;
613
+ _cachedWebsocket = class extends WebSocket {
614
+ constructor(url, options) {
615
+ let _opts = {};
616
+ if (Array.isArray(options) || typeof options === "string") {
617
+ _opts = { protocols: options };
618
+ }
619
+ else {
620
+ _opts = { ...options };
621
+ }
622
+ const proxy = getProxyForUrl(url.toString().replace(/^wss:/, "https:").replace(/^ws:/, "http:"));
623
+ super(url, { ..._opts, ...(proxy ? { proxy } : {}) });
624
+ }
625
+ };
626
+ return _cachedWebsocket;
627
+ }
347
628
  const ctor = globalThis.WebSocket;
348
629
  if (typeof ctor !== "function")
349
630
  return null;
350
631
  return ctor;
351
632
  }
352
- function headersToRecord(headers) {
353
- const out = {};
354
- for (const [key, value] of headers.entries()) {
355
- out[key] = value;
633
+ class WebSocketCloseError extends Error {
634
+ code;
635
+ reason;
636
+ wasClean;
637
+ constructor(message, options) {
638
+ super(message);
639
+ this.name = "WebSocketCloseError";
640
+ this.code = options?.code;
641
+ this.reason = options?.reason;
642
+ this.wasClean = options?.wasClean;
356
643
  }
357
- return out;
358
644
  }
359
645
  function getWebSocketReadyState(socket) {
360
646
  const readyState = socket.readyState;
@@ -382,8 +668,8 @@ function scheduleSessionWebSocketExpiry(sessionId, entry) {
382
668
  websocketSessionCache.delete(sessionId);
383
669
  }, SESSION_WEBSOCKET_CACHE_TTL_MS);
384
670
  }
385
- async function connectWebSocket(url, headers, signal) {
386
- const WebSocketCtor = getWebSocketConstructor();
671
+ async function connectWebSocket(url, headers, signal, connectTimeoutMs = DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS) {
672
+ const WebSocketCtor = await getWebSocketConstructor();
387
673
  if (!WebSocketCtor) {
388
674
  throw new Error("WebSocket transport is not available in this runtime");
389
675
  }
@@ -391,6 +677,7 @@ async function connectWebSocket(url, headers, signal) {
391
677
  delete wsHeaders["OpenAI-Beta"];
392
678
  return new Promise((resolve, reject) => {
393
679
  let settled = false;
680
+ let timeout;
394
681
  let socket;
395
682
  try {
396
683
  socket = new WebSocketCtor(url, { headers: wsHeaders });
@@ -399,61 +686,63 @@ async function connectWebSocket(url, headers, signal) {
399
686
  reject(error instanceof Error ? error : new Error(String(error)));
400
687
  return;
401
688
  }
402
- const onOpen = () => {
403
- if (settled)
404
- return;
405
- settled = true;
406
- cleanup();
407
- resolve(socket);
689
+ const cleanup = () => {
690
+ if (timeout) {
691
+ clearTimeout(timeout);
692
+ timeout = undefined;
693
+ }
694
+ socket.removeEventListener("open", onOpen);
695
+ socket.removeEventListener("error", onError);
696
+ socket.removeEventListener("close", onClose);
697
+ signal?.removeEventListener("abort", onAbort);
408
698
  };
409
- const onError = (event) => {
410
- const error = extractWebSocketError(event);
699
+ const fail = (error, closeReason) => {
411
700
  if (settled)
412
701
  return;
413
702
  settled = true;
414
703
  cleanup();
704
+ if (closeReason) {
705
+ closeWebSocketSilently(socket, 1000, closeReason);
706
+ }
415
707
  reject(error);
416
708
  };
417
- const onClose = (event) => {
418
- const error = extractWebSocketCloseError(event);
709
+ const onOpen = () => {
419
710
  if (settled)
420
711
  return;
421
712
  settled = true;
422
713
  cleanup();
423
- reject(error);
714
+ resolve(socket);
424
715
  };
425
- const onAbort = () => {
426
- if (settled)
427
- return;
428
- settled = true;
429
- cleanup();
430
- socket.close(1000, "aborted");
431
- reject(new Error("Request was aborted"));
716
+ const onError = (event) => {
717
+ fail(extractWebSocketError(event));
432
718
  };
433
- const cleanup = () => {
434
- socket.removeEventListener("open", onOpen);
435
- socket.removeEventListener("error", onError);
436
- socket.removeEventListener("close", onClose);
437
- signal?.removeEventListener("abort", onAbort);
719
+ const onClose = (event) => {
720
+ fail(extractWebSocketCloseError(event));
721
+ };
722
+ const onAbort = () => {
723
+ fail(new Error("Request was aborted"), "aborted");
438
724
  };
439
725
  socket.addEventListener("open", onOpen);
440
726
  socket.addEventListener("error", onError);
441
727
  socket.addEventListener("close", onClose);
442
728
  signal?.addEventListener("abort", onAbort);
729
+ if (connectTimeoutMs > 0) {
730
+ timeout = setTimeout(() => {
731
+ fail(new Error(`WebSocket connect timeout after ${connectTimeoutMs}ms`), "connect_timeout");
732
+ }, connectTimeoutMs);
733
+ }
734
+ if (signal?.aborted) {
735
+ onAbort();
736
+ }
443
737
  });
444
738
  }
445
- async function acquireWebSocket(url, headers, sessionId, signal) {
739
+ async function acquireWebSocket(url, headers, sessionId, signal, connectTimeoutMs) {
446
740
  if (!sessionId) {
447
- const socket = await connectWebSocket(url, headers, signal);
741
+ const socket = await connectWebSocket(url, headers, signal, connectTimeoutMs);
448
742
  return {
449
743
  socket,
450
- release: ({ keep } = {}) => {
451
- if (keep === false) {
452
- closeWebSocketSilently(socket);
453
- return;
454
- }
455
- closeWebSocketSilently(socket);
456
- },
744
+ reused: false,
745
+ release: () => closeWebSocketSilently(socket),
457
746
  };
458
747
  }
459
748
  const cached = websocketSessionCache.get(sessionId);
@@ -466,6 +755,8 @@ async function acquireWebSocket(url, headers, sessionId, signal) {
466
755
  cached.busy = true;
467
756
  return {
468
757
  socket: cached.socket,
758
+ entry: cached,
759
+ reused: true,
469
760
  release: ({ keep } = {}) => {
470
761
  if (!keep || !isWebSocketReusable(cached.socket)) {
471
762
  closeWebSocketSilently(cached.socket);
@@ -478,9 +769,10 @@ async function acquireWebSocket(url, headers, sessionId, signal) {
478
769
  };
479
770
  }
480
771
  if (cached.busy) {
481
- const socket = await connectWebSocket(url, headers, signal);
772
+ const socket = await connectWebSocket(url, headers, signal, connectTimeoutMs);
482
773
  return {
483
774
  socket,
775
+ reused: false,
484
776
  release: () => {
485
777
  closeWebSocketSilently(socket);
486
778
  },
@@ -491,11 +783,13 @@ async function acquireWebSocket(url, headers, sessionId, signal) {
491
783
  websocketSessionCache.delete(sessionId);
492
784
  }
493
785
  }
494
- const socket = await connectWebSocket(url, headers, signal);
786
+ const socket = await connectWebSocket(url, headers, signal, connectTimeoutMs);
495
787
  const entry = { socket, busy: true };
496
788
  websocketSessionCache.set(sessionId, entry);
497
789
  return {
498
790
  socket,
791
+ entry,
792
+ reused: false,
499
793
  release: ({ keep } = {}) => {
500
794
  if (!keep || !isWebSocketReusable(entry.socket)) {
501
795
  closeWebSocketSilently(entry.socket);
@@ -512,11 +806,21 @@ async function acquireWebSocket(url, headers, sessionId, signal) {
512
806
  };
513
807
  }
514
808
  function extractWebSocketError(event) {
515
- if (event && typeof event === "object" && "message" in event) {
516
- const message = event.message;
809
+ if (event && typeof event === "object") {
810
+ const message = "message" in event ? event.message : undefined;
517
811
  if (typeof message === "string" && message.length > 0) {
518
812
  return new Error(message);
519
813
  }
814
+ const nestedError = "error" in event ? event.error : undefined;
815
+ if (nestedError instanceof Error && nestedError.message.length > 0) {
816
+ return nestedError;
817
+ }
818
+ if (nestedError && typeof nestedError === "object" && "message" in nestedError) {
819
+ const nestedMessage = nestedError.message;
820
+ if (typeof nestedMessage === "string" && nestedMessage.length > 0) {
821
+ return new Error(nestedMessage);
822
+ }
823
+ }
520
824
  }
521
825
  return new Error("WebSocket error");
522
826
  }
@@ -524,9 +828,17 @@ function extractWebSocketCloseError(event) {
524
828
  if (event && typeof event === "object") {
525
829
  const code = "code" in event ? event.code : undefined;
526
830
  const reason = "reason" in event ? event.reason : undefined;
831
+ const wasClean = "wasClean" in event ? event.wasClean : undefined;
527
832
  const codeText = typeof code === "number" ? ` ${code}` : "";
528
- const reasonText = typeof reason === "string" && reason.length > 0 ? ` ${reason}` : "";
529
- return new Error(`WebSocket closed${codeText}${reasonText}`.trim());
833
+ let reasonText = typeof reason === "string" && reason.length > 0 ? ` ${reason}` : "";
834
+ if (!reasonText && code === WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE) {
835
+ reasonText = " message too big";
836
+ }
837
+ return new WebSocketCloseError(`WebSocket closed${codeText}${reasonText}`.trim(), {
838
+ code: typeof code === "number" ? code : undefined,
839
+ reason: typeof reason === "string" && reason.length > 0 ? reason : undefined,
840
+ wasClean: typeof wasClean === "boolean" ? wasClean : undefined,
841
+ });
530
842
  }
531
843
  return new Error("WebSocket closed");
532
844
  }
@@ -547,7 +859,7 @@ async function decodeWebSocketData(data) {
547
859
  }
548
860
  return null;
549
861
  }
550
- async function* parseWebSocket(socket, signal) {
862
+ async function* parseWebSocket(socket, signal, idleTimeoutMs) {
551
863
  const queue = [];
552
864
  let pending = null;
553
865
  let done = false;
@@ -562,12 +874,13 @@ async function* parseWebSocket(socket, signal) {
562
874
  };
563
875
  const onMessage = (event) => {
564
876
  void (async () => {
565
- if (!event || typeof event !== "object" || !("data" in event))
566
- return;
567
- const text = await decodeWebSocketData(event.data);
568
- if (!text)
569
- return;
877
+ let text = null;
570
878
  try {
879
+ if (!event || typeof event !== "object" || !("data" in event))
880
+ return;
881
+ text = await decodeWebSocketData(event.data);
882
+ if (!text)
883
+ return;
571
884
  const parsed = JSON.parse(text);
572
885
  const type = typeof parsed.type === "string" ? parsed.type : "";
573
886
  if (type === "response.completed" || type === "response.done" || type === "response.incomplete") {
@@ -577,7 +890,14 @@ async function* parseWebSocket(socket, signal) {
577
890
  queue.push(parsed);
578
891
  wake();
579
892
  }
580
- catch { }
893
+ catch (cause) {
894
+ failed = new CodexProtocolError(`Invalid Codex WebSocket JSON: ${formatThrownValue(cause)}`, {
895
+ cause,
896
+ payload: text,
897
+ });
898
+ done = true;
899
+ wake();
900
+ }
581
901
  })();
582
902
  };
583
903
  const onError = (event) => {
@@ -617,8 +937,23 @@ async function* parseWebSocket(socket, signal) {
617
937
  }
618
938
  if (done)
619
939
  break;
620
- await new Promise((resolve) => {
940
+ let timeout;
941
+ await new Promise((resolve, reject) => {
621
942
  pending = resolve;
943
+ if (idleTimeoutMs !== undefined && idleTimeoutMs > 0) {
944
+ timeout = setTimeout(() => {
945
+ const error = new Error(`WebSocket idle timeout after ${idleTimeoutMs}ms`);
946
+ failed = error;
947
+ done = true;
948
+ pending = null;
949
+ closeWebSocketSilently(socket, 1000, "idle_timeout");
950
+ reject(error);
951
+ }, idleTimeoutMs);
952
+ }
953
+ }).finally(() => {
954
+ if (timeout) {
955
+ clearTimeout(timeout);
956
+ }
622
957
  });
623
958
  }
624
959
  if (failed) {
@@ -635,19 +970,114 @@ async function* parseWebSocket(socket, signal) {
635
970
  signal?.removeEventListener("abort", onAbort);
636
971
  }
637
972
  }
638
- async function processWebSocketStream(url, body, headers, output, stream, model, onStart, options) {
639
- const { socket, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
973
+ function requestBodyWithoutInput(body) {
974
+ const { input: _input, previous_response_id: _previousResponseId, ...rest } = body;
975
+ return rest;
976
+ }
977
+ function responseInputsEqual(a, b) {
978
+ return JSON.stringify(a ?? []) === JSON.stringify(b ?? []);
979
+ }
980
+ function requestBodiesMatchExceptInput(a, b) {
981
+ return JSON.stringify(requestBodyWithoutInput(a)) === JSON.stringify(requestBodyWithoutInput(b));
982
+ }
983
+ function getCachedWebSocketInputDelta(body, continuation) {
984
+ if (!requestBodiesMatchExceptInput(body, continuation.lastRequestBody)) {
985
+ return undefined;
986
+ }
987
+ const currentInput = body.input ?? [];
988
+ const baseline = [...(continuation.lastRequestBody.input ?? []), ...continuation.lastResponseItems];
989
+ if (currentInput.length < baseline.length) {
990
+ return undefined;
991
+ }
992
+ const prefix = currentInput.slice(0, baseline.length);
993
+ if (!responseInputsEqual(prefix, baseline)) {
994
+ return undefined;
995
+ }
996
+ return currentInput.slice(baseline.length);
997
+ }
998
+ function buildCachedWebSocketRequestBody(entry, body) {
999
+ const continuation = entry.continuation;
1000
+ if (!continuation) {
1001
+ return body;
1002
+ }
1003
+ const delta = getCachedWebSocketInputDelta(body, continuation);
1004
+ if (!delta || !continuation.lastResponseId) {
1005
+ entry.continuation = undefined;
1006
+ return body;
1007
+ }
1008
+ return {
1009
+ ...body,
1010
+ previous_response_id: continuation.lastResponseId,
1011
+ input: delta,
1012
+ };
1013
+ }
1014
+ async function* startWebSocketOutputOnFirstEvent(events, output, stream, onStart) {
1015
+ let started = false;
1016
+ for await (const event of events) {
1017
+ if (!started) {
1018
+ started = true;
1019
+ onStart();
1020
+ stream.push({ type: "start", partial: output });
1021
+ }
1022
+ yield event;
1023
+ }
1024
+ }
1025
+ async function processWebSocketStream(url, body, headers, output, stream, model, onStart, idleTimeoutMs, websocketConnectTimeoutMs, options) {
1026
+ const { socket, entry, reused, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal, websocketConnectTimeoutMs);
640
1027
  let keepConnection = true;
1028
+ const useCachedContext = options?.transport === "websocket-cached" || options?.transport === "auto";
1029
+ // ChatGPT Codex Responses rejects `store: true` ("Store must be set to false").
1030
+ // WebSocket continuation still works via connection-scoped previous_response_id state.
1031
+ const fullBody = body;
1032
+ const requestBody = useCachedContext && entry ? buildCachedWebSocketRequestBody(entry, fullBody) : fullBody;
1033
+ const stats = options?.sessionId ? getOrCreateWebSocketDebugStats(options.sessionId) : undefined;
1034
+ if (stats) {
1035
+ stats.requests++;
1036
+ if (reused)
1037
+ stats.connectionsReused++;
1038
+ else
1039
+ stats.connectionsCreated++;
1040
+ if (useCachedContext)
1041
+ stats.cachedContextRequests++;
1042
+ if (requestBody.store === true)
1043
+ stats.storeTrueRequests++;
1044
+ stats.lastInputItems = requestBody.input?.length ?? 0;
1045
+ if (requestBody.previous_response_id) {
1046
+ stats.deltaRequests++;
1047
+ stats.lastDeltaInputItems = requestBody.input?.length ?? 0;
1048
+ stats.lastPreviousResponseId = requestBody.previous_response_id;
1049
+ }
1050
+ else {
1051
+ stats.fullContextRequests++;
1052
+ stats.lastDeltaInputItems = undefined;
1053
+ stats.lastPreviousResponseId = undefined;
1054
+ }
1055
+ }
641
1056
  try {
642
- socket.send(JSON.stringify({ type: "response.create", ...body }));
643
- onStart();
644
- stream.push({ type: "start", partial: output });
645
- await processResponsesStream(mapCodexEvents(parseWebSocket(socket, options?.signal)), output, stream, model);
1057
+ socket.send(JSON.stringify({ type: "response.create", ...requestBody }));
1058
+ await processResponsesStream(startWebSocketOutputOnFirstEvent(mapCodexEvents(parseWebSocket(socket, options?.signal, idleTimeoutMs)), output, stream, onStart), output, stream, model, {
1059
+ serviceTier: options?.serviceTier,
1060
+ resolveServiceTier: resolveCodexServiceTier,
1061
+ applyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model),
1062
+ });
646
1063
  if (options?.signal?.aborted) {
647
1064
  keepConnection = false;
648
1065
  }
1066
+ else if (useCachedContext && entry && output.responseId) {
1067
+ const responseItems = convertResponsesMessages(model, { messages: [output] }, CODEX_TOOL_CALL_PROVIDERS, {
1068
+ includeSystemPrompt: false,
1069
+ }).filter((item) => item.type !== "function_call_output");
1070
+ entry.continuation = {
1071
+ lastRequestBody: fullBody,
1072
+ lastResponseId: output.responseId,
1073
+ lastResponseItems: responseItems,
1074
+ };
1075
+ }
649
1076
  }
650
1077
  catch (error) {
1078
+ if (entry) {
1079
+ entry.continuation = undefined;
1080
+ }
651
1081
  keepConnection = false;
652
1082
  throw error;
653
1083
  }
@@ -712,7 +1142,7 @@ function buildBaseCodexHeaders(initHeaders, additionalHeaders, accountId, token)
712
1142
  }
713
1143
  headers.set("Authorization", `Bearer ${token}`);
714
1144
  headers.set("chatgpt-account-id", accountId);
715
- headers.set("originator", "draht");
1145
+ headers.set("originator", "pi");
716
1146
  const userAgent = _os ? `pi (${_os.platform()} ${_os.release()}; ${_os.arch()})` : "pi (browser)";
717
1147
  headers.set("User-Agent", userAgent);
718
1148
  return headers;
@@ -723,7 +1153,8 @@ function buildSSEHeaders(initHeaders, additionalHeaders, accountId, token, sessi
723
1153
  headers.set("accept", "text/event-stream");
724
1154
  headers.set("content-type", "application/json");
725
1155
  if (sessionId) {
726
- headers.set("session_id", sessionId);
1156
+ headers.set("session-id", sessionId);
1157
+ headers.set("x-client-request-id", sessionId);
727
1158
  }
728
1159
  return headers;
729
1160
  }
@@ -735,7 +1166,7 @@ function buildWebSocketHeaders(initHeaders, additionalHeaders, accountId, token,
735
1166
  headers.delete("openai-beta");
736
1167
  headers.set("OpenAI-Beta", OPENAI_BETA_RESPONSES_WEBSOCKETS);
737
1168
  headers.set("x-client-request-id", requestId);
738
- headers.set("session_id", requestId);
1169
+ headers.set("session-id", requestId);
739
1170
  return headers;
740
1171
  }
741
1172
  //# sourceMappingURL=openai-codex-responses.js.map