@jecruz/merc-ai 0.1.4 → 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 (111) hide show
  1. package/README.md +34 -7
  2. package/dist/env-api-keys.d.ts.map +1 -1
  3. package/dist/env-api-keys.js +1 -1
  4. package/dist/env-api-keys.js.map +1 -1
  5. package/dist/image-models.generated.d.ts +345 -0
  6. package/dist/image-models.generated.d.ts.map +1 -1
  7. package/dist/image-models.generated.js +348 -3
  8. package/dist/image-models.generated.js.map +1 -1
  9. package/dist/index.d.ts +3 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +3 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/models.d.ts.map +1 -1
  14. package/dist/models.generated.d.ts +15282 -3882
  15. package/dist/models.generated.d.ts.map +1 -1
  16. package/dist/models.generated.js +18368 -10964
  17. package/dist/models.generated.js.map +1 -1
  18. package/dist/models.js +4 -1
  19. package/dist/models.js.map +1 -1
  20. package/dist/providers/amazon-bedrock.d.ts.map +1 -1
  21. package/dist/providers/amazon-bedrock.js +188 -19
  22. package/dist/providers/amazon-bedrock.js.map +1 -1
  23. package/dist/providers/anthropic.d.ts.map +1 -1
  24. package/dist/providers/anthropic.js +101 -34
  25. package/dist/providers/anthropic.js.map +1 -1
  26. package/dist/providers/azure-openai-responses.d.ts +2 -0
  27. package/dist/providers/azure-openai-responses.d.ts.map +1 -1
  28. package/dist/providers/azure-openai-responses.js +8 -3
  29. package/dist/providers/azure-openai-responses.js.map +1 -1
  30. package/dist/providers/google-shared.d.ts +15 -4
  31. package/dist/providers/google-shared.d.ts.map +1 -1
  32. package/dist/providers/google-shared.js +80 -15
  33. package/dist/providers/google-shared.js.map +1 -1
  34. package/dist/providers/google-vertex.d.ts.map +1 -1
  35. package/dist/providers/google-vertex.js +23 -60
  36. package/dist/providers/google-vertex.js.map +1 -1
  37. package/dist/providers/google.d.ts.map +1 -1
  38. package/dist/providers/google.js +19 -68
  39. package/dist/providers/google.js.map +1 -1
  40. package/dist/providers/mistral.d.ts +1 -1
  41. package/dist/providers/mistral.d.ts.map +1 -1
  42. package/dist/providers/mistral.js +44 -60
  43. package/dist/providers/mistral.js.map +1 -1
  44. package/dist/providers/openai-codex-responses.d.ts.map +1 -1
  45. package/dist/providers/openai-codex-responses.js +250 -124
  46. package/dist/providers/openai-codex-responses.js.map +1 -1
  47. package/dist/providers/openai-completions.d.ts.map +1 -1
  48. package/dist/providers/openai-completions.js +94 -46
  49. package/dist/providers/openai-completions.js.map +1 -1
  50. package/dist/providers/openai-prompt-cache.d.ts +3 -0
  51. package/dist/providers/openai-prompt-cache.d.ts.map +1 -0
  52. package/dist/providers/openai-prompt-cache.js +10 -0
  53. package/dist/providers/openai-prompt-cache.js.map +1 -0
  54. package/dist/providers/openai-responses-shared.d.ts.map +1 -1
  55. package/dist/providers/openai-responses-shared.js +154 -211
  56. package/dist/providers/openai-responses-shared.js.map +1 -1
  57. package/dist/providers/openai-responses.d.ts.map +1 -1
  58. package/dist/providers/openai-responses.js +15 -9
  59. package/dist/providers/openai-responses.js.map +1 -1
  60. package/dist/providers/openrouter-images.d.ts.map +1 -1
  61. package/dist/providers/openrouter-images.js +5 -2
  62. package/dist/providers/openrouter-images.js.map +1 -1
  63. package/dist/providers/simple-options.d.ts +15 -3
  64. package/dist/providers/simple-options.d.ts.map +1 -1
  65. package/dist/providers/simple-options.js +36 -4
  66. package/dist/providers/simple-options.js.map +1 -1
  67. package/dist/types.d.ts +35 -2
  68. package/dist/types.d.ts.map +1 -1
  69. package/dist/types.js.map +1 -1
  70. package/dist/utils/error-body.d.ts +5 -0
  71. package/dist/utils/error-body.d.ts.map +1 -1
  72. package/dist/utils/error-body.js +53 -5
  73. package/dist/utils/error-body.js.map +1 -1
  74. package/dist/utils/estimate.d.ts +22 -0
  75. package/dist/utils/estimate.d.ts.map +1 -0
  76. package/dist/utils/estimate.js +104 -0
  77. package/dist/utils/estimate.js.map +1 -0
  78. package/dist/utils/event-stream.d.ts.map +1 -1
  79. package/dist/utils/event-stream.js +25 -7
  80. package/dist/utils/event-stream.js.map +1 -1
  81. package/dist/utils/json-parse.d.ts +3 -0
  82. package/dist/utils/json-parse.d.ts.map +1 -1
  83. package/dist/utils/json-parse.js +15 -0
  84. package/dist/utils/json-parse.js.map +1 -1
  85. package/dist/utils/oauth/anthropic.d.ts.map +1 -1
  86. package/dist/utils/oauth/anthropic.js +16 -10
  87. package/dist/utils/oauth/anthropic.js.map +1 -1
  88. package/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  89. package/dist/utils/oauth/github-copilot.js +57 -21
  90. package/dist/utils/oauth/github-copilot.js.map +1 -1
  91. package/dist/utils/oauth/openai-codex.d.ts.map +1 -1
  92. package/dist/utils/oauth/openai-codex.js +73 -16
  93. package/dist/utils/oauth/openai-codex.js.map +1 -1
  94. package/dist/utils/oauth/types.d.ts +2 -0
  95. package/dist/utils/oauth/types.d.ts.map +1 -1
  96. package/dist/utils/oauth/types.js.map +1 -1
  97. package/dist/utils/overflow.d.ts +10 -1
  98. package/dist/utils/overflow.d.ts.map +1 -1
  99. package/dist/utils/overflow.js +47 -7
  100. package/dist/utils/overflow.js.map +1 -1
  101. package/dist/utils/provider-env.d.ts.map +1 -1
  102. package/dist/utils/provider-env.js +45 -10
  103. package/dist/utils/provider-env.js.map +1 -1
  104. package/dist/utils/tool-call-id.d.ts +11 -0
  105. package/dist/utils/tool-call-id.d.ts.map +1 -0
  106. package/dist/utils/tool-call-id.js +96 -0
  107. package/dist/utils/tool-call-id.js.map +1 -0
  108. package/dist/utils/validation.d.ts.map +1 -1
  109. package/dist/utils/validation.js +9 -0
  110. package/dist/utils/validation.js.map +1 -1
  111. package/package.json +6 -3
@@ -1,21 +1,23 @@
1
- // NEVER convert to top-level runtime imports - breaks browser/Vite builds (web-ui)
2
- let _os = null;
3
1
  const dynamicImport = (specifier) => import(specifier);
4
- const NODE_OS_SPECIFIER = "node:" + "os";
5
- if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
6
- dynamicImport(NODE_OS_SPECIFIER).then((m) => {
7
- _os = m;
8
- });
2
+ function loadNodeOs() {
3
+ if (typeof process === "undefined" || !(process.versions?.node || process.versions?.bun)) {
4
+ return null;
5
+ }
6
+ return process.getBuiltinModule?.("node:os") ?? null;
9
7
  }
8
+ // NEVER convert to top-level runtime imports - breaks browser/Vite builds (web-ui)
9
+ const _os = loadNodeOs();
10
+ import { v7 as uuidv7 } from "uuid";
10
11
  import { getEnvApiKey } from "../env-api-keys.js";
11
12
  import { clampThinkingLevel } from "../models.js";
12
13
  import { registerSessionResourceCleanup } from "../session-resources.js";
13
14
  import { splitDeferredTools } from "../utils/deferred-tools.js";
14
15
  import { appendAssistantMessageDiagnostic, createAssistantMessageDiagnostic, formatThrownValue, } from "../utils/diagnostics.js";
15
- import { formatProviderError, normalizeProviderError } from "../utils/error-body.js";
16
+ import { formatProviderError, isDeterministicProviderError, normalizeProviderError } from "../utils/error-body.js";
16
17
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
17
18
  import { headersToRecord } from "../utils/headers.js";
18
19
  import { resolveProviderProxyUrl } from "../utils/provider-env.js";
20
+ import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
19
21
  import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js";
20
22
  import { buildBaseOptions } from "./simple-options.js";
21
23
  // ============================================================================
@@ -23,11 +25,14 @@ import { buildBaseOptions } from "./simple-options.js";
23
25
  // ============================================================================
24
26
  const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
25
27
  const JWT_CLAIM_PATH = "https://api.openai.com/auth";
26
- const DEFAULT_MAX_RETRIES = 3;
28
+ const DEFAULT_MAX_RETRIES = 0;
27
29
  const BASE_DELAY_MS = 1000;
30
+ const DEFAULT_MAX_RETRY_DELAY_MS = 60_000;
28
31
  const DEFAULT_SSE_HEADER_TIMEOUT_MS = 20_000;
32
+ const DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS = 15_000;
29
33
  const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
30
34
  const WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009;
35
+ const WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE = "websocket_connection_limit_reached";
31
36
  const CODEX_RESPONSE_STATUSES = new Set([
32
37
  "completed",
33
38
  "incomplete",
@@ -36,6 +41,7 @@ const CODEX_RESPONSE_STATUSES = new Set([
36
41
  "queued",
37
42
  "in_progress",
38
43
  ]);
44
+ // Codex requests never carry max_output_tokens, so supportsMaxOutputTokens does not apply here.
39
45
  function getCompat(model) {
40
46
  return {
41
47
  supportsDeveloperRole: model.compat?.supportsDeveloperRole ?? true,
@@ -47,17 +53,32 @@ function getCompat(model) {
47
53
  function defaultSupportsToolSearch(model) {
48
54
  if (model.provider !== "openai" && model.provider !== "openai-codex")
49
55
  return false;
50
- return /gpt-5\.(4|5|6)/.test(model.id);
56
+ return /(?:^|\/)gpt-(?:5\.[456]|6)(?:[.-]|$)/.test(model.id);
51
57
  }
52
58
  // ============================================================================
53
59
  // Retry Helpers
54
60
  // ============================================================================
61
+ function isTerminalRateLimitError(errorText) {
62
+ return /GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(errorText);
63
+ }
55
64
  function isRetryableError(status, errorText) {
65
+ if (status === 429 && isTerminalRateLimitError(errorText)) {
66
+ return false;
67
+ }
56
68
  if (status === 429 || status === 500 || status === 502 || status === 503 || status === 504) {
57
69
  return true;
58
70
  }
59
71
  return /rate.?limit|overloaded|service.?unavailable|upstream.?connect|connection.?refused/i.test(errorText);
60
72
  }
73
+ class RetryDelayExceededError extends Error {
74
+ }
75
+ function validateRetryDelayMs(delayMs, options) {
76
+ const maxRetryDelayMs = options?.maxRetryDelayMs ?? DEFAULT_MAX_RETRY_DELAY_MS;
77
+ if (maxRetryDelayMs > 0 && delayMs > maxRetryDelayMs) {
78
+ throw new RetryDelayExceededError(`Server requested ${Math.ceil(delayMs / 1000)}s retry delay (max: ${Math.ceil(maxRetryDelayMs / 1000)}s)`);
79
+ }
80
+ return delayMs;
81
+ }
61
82
  function sleep(ms, signal) {
62
83
  return new Promise((resolve, reject) => {
63
84
  if (signal?.aborted) {
@@ -93,6 +114,14 @@ function parseRetryAfterDelay(response) {
93
114
  }
94
115
  return undefined;
95
116
  }
117
+ function normalizeTimeoutMs(value) {
118
+ if (value === undefined)
119
+ return undefined;
120
+ if (!Number.isFinite(value) || value < 0) {
121
+ throw new Error(`Invalid timeoutMs: ${String(value)}`);
122
+ }
123
+ return Math.floor(value);
124
+ }
96
125
  // ============================================================================
97
126
  // Main Stream Function
98
127
  // ============================================================================
@@ -122,58 +151,82 @@ export const streamOpenAICodexResponses = (model, context, options) => {
122
151
  throw new Error(`No API key for provider: ${model.provider}`);
123
152
  }
124
153
  const accountId = extractAccountId(apiKey);
125
- let body = buildRequestBody(model, context, options);
154
+ const cacheSessionId = options?.cacheRetention === "none" ? undefined : options?.sessionId;
155
+ let body = buildRequestBody(model, context, options, cacheSessionId);
126
156
  const nextBody = await options?.onPayload?.(body, model);
127
157
  if (nextBody !== undefined) {
128
158
  body = nextBody;
129
159
  }
130
- const websocketRequestId = options?.sessionId || createCodexRequestId();
131
- const sseHeaders = buildSSEHeaders(model.headers, options?.headers, accountId, apiKey, options?.sessionId);
160
+ const websocketRequestId = cacheSessionId || uuidv7();
161
+ const sseHeaders = buildSSEHeaders(model.headers, options?.headers, accountId, apiKey, cacheSessionId);
132
162
  const websocketHeaders = buildWebSocketHeaders(model.headers, options?.headers, accountId, apiKey, websocketRequestId);
133
163
  const bodyJson = JSON.stringify(body);
164
+ const idleTimeoutMs = normalizeTimeoutMs(options?.timeoutMs);
165
+ const websocketConnectTimeoutMs = normalizeTimeoutMs(options?.websocketConnectTimeoutMs);
134
166
  const transport = options?.transport || "auto";
135
- const websocketDisabledForSession = transport !== "sse" && isWebSocketSseFallbackActive(options?.sessionId);
167
+ const websocketDisabledForSession = transport !== "sse" && isWebSocketSseFallbackActive(cacheSessionId);
136
168
  if (websocketDisabledForSession) {
137
- recordWebSocketSseFallback(options?.sessionId);
169
+ recordWebSocketSseFallback(cacheSessionId);
138
170
  }
139
171
  if (transport !== "sse") {
140
172
  let websocketReceivedEvent = false;
141
- try {
142
- await processWebSocketStream(resolveCodexWebSocketUrl(model.baseUrl), body, websocketHeaders, output, stream, model, () => { }, () => {
143
- websocketReceivedEvent = true;
144
- }, options);
145
- if (options?.signal?.aborted) {
146
- throw new Error("Request was aborted");
147
- }
148
- stream.push({
149
- type: "done",
150
- reason: output.stopReason,
151
- message: output,
152
- });
153
- stream.end();
154
- return;
155
- }
156
- catch (error) {
157
- // In auto mode, ChatGPT's Codex WebSocket can occasionally close
158
- // abnormally (1006) after `response.create` is sent but before any
159
- // response event arrives. That is still safe to retry over SSE using
160
- // the original full request body. Once any response event has arrived,
161
- // do not retry: replaying could duplicate partial assistant/tool output.
162
- if (transport === "websocket" || transport === "websocket-cached" || websocketReceivedEvent) {
163
- throw error;
173
+ let retriedWebSocketConnectionLimit = false;
174
+ let websocketStartPushed = false;
175
+ while (true) {
176
+ websocketReceivedEvent = false;
177
+ try {
178
+ await processWebSocketStream(resolveCodexWebSocketUrl(model.baseUrl), body, websocketHeaders, output, stream, model, () => {
179
+ if (websocketStartPushed)
180
+ return;
181
+ websocketStartPushed = true;
182
+ stream.push({ type: "start", partial: output });
183
+ }, () => {
184
+ websocketReceivedEvent = true;
185
+ }, cacheSessionId, idleTimeoutMs, websocketConnectTimeoutMs, options);
186
+ if (options?.signal?.aborted) {
187
+ throw new Error("Request was aborted");
188
+ }
189
+ stream.push({
190
+ type: "done",
191
+ reason: output.stopReason,
192
+ message: output,
193
+ });
194
+ stream.end();
195
+ return;
164
196
  }
165
- appendAssistantMessageDiagnostic(output, createAssistantMessageDiagnostic("provider_transport_failure", error, {
166
- configuredTransport: transport,
167
- fallbackTransport: websocketReceivedEvent ? undefined : "sse",
168
- eventsEmitted: websocketReceivedEvent,
169
- phase: websocketReceivedEvent ? "after_message_stream_start" : "before_message_stream_start",
170
- requestBytes: new TextEncoder().encode(bodyJson).byteLength,
171
- }));
172
- recordWebSocketFailure(options?.sessionId, error);
173
- if (websocketReceivedEvent) {
174
- throw error;
197
+ catch (error) {
198
+ // ChatGPT rejects a new WebSocket with websocket_connection_limit_reached when the
199
+ // account already holds its maximum number of connections. Nothing was streamed yet,
200
+ // so reconnect once instead of surfacing the error or dropping to SSE.
201
+ if (!options?.signal?.aborted &&
202
+ !websocketReceivedEvent &&
203
+ !retriedWebSocketConnectionLimit &&
204
+ isWebSocketConnectionLimitReachedError(error)) {
205
+ retriedWebSocketConnectionLimit = true;
206
+ continue;
207
+ }
208
+ // In auto mode, ChatGPT's Codex WebSocket can occasionally close
209
+ // abnormally (1006) after `response.create` is sent but before any
210
+ // response event arrives. That is still safe to retry over SSE using
211
+ // the original full request body. Once any response event has arrived,
212
+ // do not retry: replaying could duplicate partial assistant/tool output.
213
+ if (transport === "websocket" || transport === "websocket-cached" || websocketReceivedEvent) {
214
+ throw error;
215
+ }
216
+ appendAssistantMessageDiagnostic(output, createAssistantMessageDiagnostic("provider_transport_failure", error, {
217
+ configuredTransport: transport,
218
+ fallbackTransport: websocketReceivedEvent ? undefined : "sse",
219
+ eventsEmitted: websocketReceivedEvent,
220
+ phase: websocketReceivedEvent ? "after_message_stream_start" : "before_message_stream_start",
221
+ requestBytes: new TextEncoder().encode(bodyJson).byteLength,
222
+ }));
223
+ recordWebSocketFailure(cacheSessionId, error);
224
+ if (websocketReceivedEvent) {
225
+ throw error;
226
+ }
227
+ recordWebSocketSseFallback(cacheSessionId);
228
+ break;
175
229
  }
176
- recordWebSocketSseFallback(options?.sessionId);
177
230
  }
178
231
  }
179
232
  // Fetch with retry logic for rate limits and transient errors
@@ -186,10 +239,11 @@ export const streamOpenAICodexResponses = (model, context, options) => {
186
239
  }
187
240
  const headerTimeoutController = new AbortController();
188
241
  let timedOut = false;
242
+ const headerTimeoutMs = options?.timeoutMs ?? DEFAULT_SSE_HEADER_TIMEOUT_MS;
189
243
  const headerTimeout = setTimeout(() => {
190
244
  timedOut = true;
191
245
  headerTimeoutController.abort();
192
- }, DEFAULT_SSE_HEADER_TIMEOUT_MS);
246
+ }, headerTimeoutMs);
193
247
  const onAbort = () => {
194
248
  headerTimeoutController.abort();
195
249
  };
@@ -206,22 +260,25 @@ export const streamOpenAICodexResponses = (model, context, options) => {
206
260
  break;
207
261
  }
208
262
  const errorText = await response.text();
209
- if (attempt < maxRetries && isRetryableError(response.status, errorText)) {
210
- const delayMs = parseRetryAfterDelay(response) ?? BASE_DELAY_MS * 2 ** attempt;
263
+ const info = await parseErrorResponse(new Response(errorText, {
264
+ status: response.status,
265
+ statusText: response.statusText,
266
+ }));
267
+ if (attempt < maxRetries &&
268
+ !isDeterministicProviderError(info.errorMetadata) &&
269
+ isRetryableError(response.status, errorText)) {
270
+ const retryAfterDelayMs = parseRetryAfterDelay(response);
271
+ const delayMs = retryAfterDelayMs === undefined
272
+ ? BASE_DELAY_MS * 2 ** attempt
273
+ : validateRetryDelayMs(retryAfterDelayMs, options);
211
274
  await sleep(delayMs, options?.signal);
212
275
  continue;
213
276
  }
214
- // Parse error for friendly message on final attempt or non-retryable error
215
- const fakeResponse = new Response(errorText, {
216
- status: response.status,
217
- statusText: response.statusText,
218
- });
219
- const info = await parseErrorResponse(fakeResponse);
220
- throw new Error(info.friendlyMessage || info.message);
277
+ throw new CodexApiError(info.friendlyMessage || info.message, info.errorMetadata);
221
278
  }
222
279
  catch (error) {
223
280
  if (timedOut && !options?.signal?.aborted) {
224
- throw new Error(`Codex SSE response headers timed out after ${DEFAULT_SSE_HEADER_TIMEOUT_MS}ms`);
281
+ throw new Error(`Codex SSE response headers timed out after ${headerTimeoutMs}ms`);
225
282
  }
226
283
  if (error instanceof Error) {
227
284
  if (error.name === "AbortError" || error.message === "Request was aborted") {
@@ -230,7 +287,10 @@ export const streamOpenAICodexResponses = (model, context, options) => {
230
287
  }
231
288
  lastError = error instanceof Error ? error : new Error(String(error));
232
289
  // Network errors are retryable
233
- if (attempt < maxRetries && !lastError.message.includes("usage limit")) {
290
+ if (attempt < maxRetries &&
291
+ !(lastError instanceof RetryDelayExceededError) &&
292
+ !isDeterministicProviderError(normalizeProviderError(lastError).errorMetadata) &&
293
+ !lastError.message.includes("usage limit")) {
234
294
  const delayMs = BASE_DELAY_MS * 2 ** attempt;
235
295
  await sleep(delayMs, options?.signal);
236
296
  continue;
@@ -262,7 +322,9 @@ export const streamOpenAICodexResponses = (model, context, options) => {
262
322
  delete block.partialJson;
263
323
  }
264
324
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
265
- output.errorMessage = formatProviderError(normalizeProviderError(error));
325
+ const normalized = normalizeProviderError(error);
326
+ output.errorMetadata = normalized.errorMetadata;
327
+ output.errorMessage = formatProviderError(normalized);
266
328
  stream.push({ type: "error", reason: output.stopReason, error: output });
267
329
  stream.end();
268
330
  }
@@ -274,7 +336,7 @@ export const streamSimpleOpenAICodexResponses = (model, context, options) => {
274
336
  if (!apiKey) {
275
337
  throw new Error(`No API key for provider: ${model.provider}`);
276
338
  }
277
- const base = buildBaseOptions(model, options, apiKey);
339
+ const base = buildBaseOptions(model, context, options, apiKey);
278
340
  const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
279
341
  const reasoningEffort = clampedReasoning === "off" ? undefined : clampedReasoning;
280
342
  return streamOpenAICodexResponses(model, context, {
@@ -285,7 +347,7 @@ export const streamSimpleOpenAICodexResponses = (model, context, options) => {
285
347
  // ============================================================================
286
348
  // Request Building
287
349
  // ============================================================================
288
- function buildRequestBody(model, context, options) {
350
+ function buildRequestBody(model, context, options, cacheSessionId) {
289
351
  const compat = getCompat(model);
290
352
  const toolPlacement = splitDeferredTools(context, compat.supportsToolSearch);
291
353
  const messages = convertResponsesMessages(model, context, CODEX_TOOL_CALL_PROVIDERS, {
@@ -300,7 +362,7 @@ function buildRequestBody(model, context, options) {
300
362
  input: messages,
301
363
  text: { verbosity: options?.textVerbosity || "low" },
302
364
  include: ["reasoning.encrypted_content"],
303
- prompt_cache_key: options?.sessionId,
365
+ prompt_cache_key: clampOpenAIPromptCacheKey(cacheSessionId),
304
366
  tool_choice: options?.toolChoice ?? "auto",
305
367
  parallel_tool_calls: true,
306
368
  };
@@ -315,7 +377,9 @@ function buildRequestBody(model, context, options) {
315
377
  }
316
378
  if (options?.reasoningEffort !== undefined) {
317
379
  const effort = options.reasoningEffort === "none"
318
- ? (model.thinkingLevelMap?.off ?? "none")
380
+ ? model.thinkingLevelMap?.off === undefined
381
+ ? "none"
382
+ : model.thinkingLevelMap.off
319
383
  : (model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort);
320
384
  if (effort !== null) {
321
385
  body.reasoning = {
@@ -324,6 +388,9 @@ function buildRequestBody(model, context, options) {
324
388
  };
325
389
  }
326
390
  }
391
+ else if (model.reasoning && model.thinkingLevelMap?.off !== null) {
392
+ body.reasoning = { effort: model.thinkingLevelMap?.off ?? "none" };
393
+ }
327
394
  return body;
328
395
  }
329
396
  function getServiceTierCostMultiplier(model, serviceTier) {
@@ -381,11 +448,13 @@ async function processStream(response, output, stream, model, options) {
381
448
  }
382
449
  class CodexApiError extends Error {
383
450
  code;
451
+ type;
384
452
  payload;
385
453
  constructor(message, options) {
386
454
  super(message);
387
455
  this.name = "CodexApiError";
388
456
  this.code = options?.code;
457
+ this.type = options?.type;
389
458
  this.payload = options?.payload;
390
459
  this.cause = options?.cause;
391
460
  }
@@ -402,24 +471,43 @@ class CodexProtocolError extends Error {
402
471
  function _isCodexNonTransportError(error) {
403
472
  return error instanceof CodexApiError || error instanceof CodexProtocolError;
404
473
  }
474
+ function isWebSocketConnectionLimitReachedError(error) {
475
+ return error instanceof CodexApiError && error.code === WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE;
476
+ }
477
+ function isWebSocketConnectionLimitReachedEvent(event) {
478
+ return event.type === "error" && extractCodexEventError(event).code === WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE;
479
+ }
480
+ function extractCodexEventError(event) {
481
+ const nested = event.error && typeof event.error === "object" ? event.error : undefined;
482
+ return {
483
+ ...normalizeProviderError({ code: event.code, error: nested }).errorMetadata,
484
+ message: typeof event.message === "string"
485
+ ? event.message
486
+ : typeof nested?.message === "string"
487
+ ? nested.message
488
+ : undefined,
489
+ };
490
+ }
405
491
  async function* mapCodexEvents(events) {
406
492
  for await (const event of events) {
407
493
  const type = typeof event.type === "string" ? event.type : undefined;
408
494
  if (!type)
409
495
  continue;
410
496
  if (type === "error") {
411
- const code = event.code || "";
412
- const message = event.message || "";
497
+ const { code, type: errorType, message } = extractCodexEventError(event);
413
498
  throw new CodexApiError(`Codex error: ${message || code || JSON.stringify(event)}`, {
414
- code: code || undefined,
499
+ code,
500
+ type: errorType,
415
501
  payload: event,
416
502
  });
417
503
  }
418
504
  if (type === "response.failed") {
419
505
  const response = event.response;
420
- const code = response?.error?.code;
421
506
  const message = response?.error?.message;
422
- throw new CodexApiError(message || "Codex response failed", { code, payload: event });
507
+ throw new CodexApiError(message || "Codex response failed", {
508
+ ...normalizeProviderError(response?.error).errorMetadata,
509
+ payload: event,
510
+ });
423
511
  }
424
512
  if (type === "response.done" || type === "response.completed" || type === "response.incomplete") {
425
513
  const response = event.response;
@@ -459,9 +547,10 @@ async function* parseSSE(response, signal) {
459
547
  if (signal?.aborted) {
460
548
  throw new Error("Request was aborted");
461
549
  }
462
- if (done)
463
- break;
464
- buffer += decoder.decode(value, { stream: true });
550
+ buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });
551
+ // Treat EOF as terminating the residual SSE frame.
552
+ if (done && buffer.trim())
553
+ buffer += "\n\n";
465
554
  let idx = buffer.indexOf("\n\n");
466
555
  while (idx !== -1) {
467
556
  const chunk = buffer.slice(0, idx);
@@ -486,6 +575,8 @@ async function* parseSSE(response, signal) {
486
575
  }
487
576
  idx = buffer.indexOf("\n\n");
488
577
  }
578
+ if (done)
579
+ break;
489
580
  }
490
581
  }
491
582
  finally {
@@ -682,7 +773,7 @@ function scheduleSessionWebSocketExpiry(sessionId, entry) {
682
773
  websocketSessionCache.delete(sessionId);
683
774
  }, SESSION_WEBSOCKET_CACHE_TTL_MS);
684
775
  }
685
- async function connectWebSocket(url, headers, signal, env) {
776
+ async function connectWebSocket(url, headers, signal, env, connectTimeoutMs = DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_MS) {
686
777
  const WebSocketCtor = await getWebSocketConstructor(env);
687
778
  if (!WebSocketCtor) {
688
779
  throw new Error("WebSocket transport is not available in this runtime");
@@ -691,6 +782,7 @@ async function connectWebSocket(url, headers, signal, env) {
691
782
  delete wsHeaders["OpenAI-Beta"];
692
783
  return new Promise((resolve, reject) => {
693
784
  let settled = false;
785
+ let timeout;
694
786
  let socket;
695
787
  try {
696
788
  socket = new WebSocketCtor(url, { headers: wsHeaders });
@@ -699,52 +791,59 @@ async function connectWebSocket(url, headers, signal, env) {
699
791
  reject(error instanceof Error ? error : new Error(String(error)));
700
792
  return;
701
793
  }
702
- const onOpen = () => {
703
- if (settled)
704
- return;
705
- settled = true;
706
- cleanup();
707
- resolve(socket);
794
+ const cleanup = () => {
795
+ if (timeout) {
796
+ clearTimeout(timeout);
797
+ timeout = undefined;
798
+ }
799
+ socket.removeEventListener("open", onOpen);
800
+ socket.removeEventListener("error", onError);
801
+ socket.removeEventListener("close", onClose);
802
+ signal?.removeEventListener("abort", onAbort);
708
803
  };
709
- const onError = (event) => {
710
- const error = extractWebSocketError(event);
804
+ const fail = (error, closeReason) => {
711
805
  if (settled)
712
806
  return;
713
807
  settled = true;
714
808
  cleanup();
809
+ if (closeReason) {
810
+ closeWebSocketSilently(socket, 1000, closeReason);
811
+ }
715
812
  reject(error);
716
813
  };
717
- const onClose = (event) => {
718
- const error = extractWebSocketCloseError(event);
814
+ const onOpen = () => {
719
815
  if (settled)
720
816
  return;
721
817
  settled = true;
722
818
  cleanup();
723
- reject(error);
819
+ resolve(socket);
724
820
  };
725
- const onAbort = () => {
726
- if (settled)
727
- return;
728
- settled = true;
729
- cleanup();
730
- socket.close(1000, "aborted");
731
- reject(new Error("Request was aborted"));
821
+ const onError = (event) => {
822
+ fail(extractWebSocketError(event));
732
823
  };
733
- const cleanup = () => {
734
- socket.removeEventListener("open", onOpen);
735
- socket.removeEventListener("error", onError);
736
- socket.removeEventListener("close", onClose);
737
- signal?.removeEventListener("abort", onAbort);
824
+ const onClose = (event) => {
825
+ fail(extractWebSocketCloseError(event));
826
+ };
827
+ const onAbort = () => {
828
+ fail(new Error("Request was aborted"), "aborted");
738
829
  };
739
830
  socket.addEventListener("open", onOpen);
740
831
  socket.addEventListener("error", onError);
741
832
  socket.addEventListener("close", onClose);
742
833
  signal?.addEventListener("abort", onAbort);
834
+ if (connectTimeoutMs > 0) {
835
+ timeout = setTimeout(() => {
836
+ fail(new Error(`WebSocket connect timeout after ${connectTimeoutMs}ms`), "connect_timeout");
837
+ }, connectTimeoutMs);
838
+ }
839
+ if (signal?.aborted) {
840
+ onAbort();
841
+ }
743
842
  });
744
843
  }
745
- async function acquireWebSocket(url, headers, sessionId, signal, env) {
844
+ async function acquireWebSocket(url, headers, sessionId, signal, env, connectTimeoutMs) {
746
845
  if (!sessionId) {
747
- const socket = await connectWebSocket(url, headers, signal, env);
846
+ const socket = await connectWebSocket(url, headers, signal, env, connectTimeoutMs);
748
847
  return {
749
848
  socket,
750
849
  reused: false,
@@ -758,7 +857,20 @@ async function acquireWebSocket(url, headers, sessionId, signal, env) {
758
857
  };
759
858
  }
760
859
  const proxyKey = getScopedProxyKey(env);
860
+ const accountId = headers.get("chatgpt-account-id") ?? undefined;
761
861
  const cached = websocketSessionCache.get(sessionId);
862
+ if (cached && cached.accountId !== accountId) {
863
+ // A cached socket is authenticated for one account; never reuse it for another
864
+ // account sharing the session id. Leave the cached entry untouched.
865
+ const socket = await connectWebSocket(url, headers, signal, env, connectTimeoutMs);
866
+ return {
867
+ socket,
868
+ reused: false,
869
+ release: () => {
870
+ closeWebSocketSilently(socket);
871
+ },
872
+ };
873
+ }
762
874
  if (cached) {
763
875
  if (cached.proxyKey !== proxyKey) {
764
876
  closeWebSocketSilently(cached.socket, 1000, "proxy_config_changed");
@@ -793,7 +905,7 @@ async function acquireWebSocket(url, headers, sessionId, signal, env) {
793
905
  };
794
906
  }
795
907
  if (cached.busy) {
796
- const socket = await connectWebSocket(url, headers, signal, env);
908
+ const socket = await connectWebSocket(url, headers, signal, env, connectTimeoutMs);
797
909
  return {
798
910
  socket,
799
911
  reused: false,
@@ -808,8 +920,8 @@ async function acquireWebSocket(url, headers, sessionId, signal, env) {
808
920
  }
809
921
  }
810
922
  }
811
- const socket = await connectWebSocket(url, headers, signal, env);
812
- const entry = { socket, busy: true, createdAt: Date.now(), proxyKey };
923
+ const socket = await connectWebSocket(url, headers, signal, env, connectTimeoutMs);
924
+ const entry = { socket, busy: true, createdAt: Date.now(), proxyKey, accountId };
813
925
  websocketSessionCache.set(sessionId, entry);
814
926
  return {
815
927
  socket,
@@ -884,7 +996,7 @@ async function decodeWebSocketData(data) {
884
996
  }
885
997
  return null;
886
998
  }
887
- async function* parseWebSocket(socket, signal) {
999
+ async function* parseWebSocket(socket, signal, idleTimeoutMs) {
888
1000
  const queue = [];
889
1001
  let pending = null;
890
1002
  let done = false;
@@ -962,8 +1074,23 @@ async function* parseWebSocket(socket, signal) {
962
1074
  }
963
1075
  if (done)
964
1076
  break;
965
- await new Promise((resolve) => {
1077
+ let timeout;
1078
+ await new Promise((resolve, reject) => {
966
1079
  pending = resolve;
1080
+ if (idleTimeoutMs !== undefined && idleTimeoutMs > 0) {
1081
+ timeout = setTimeout(() => {
1082
+ const error = new Error(`WebSocket idle timeout after ${idleTimeoutMs}ms`);
1083
+ failed = error;
1084
+ done = true;
1085
+ pending = null;
1086
+ closeWebSocketSilently(socket, 1000, "idle_timeout");
1087
+ reject(error);
1088
+ }, idleTimeoutMs);
1089
+ }
1090
+ }).finally(() => {
1091
+ if (timeout) {
1092
+ clearTimeout(timeout);
1093
+ }
967
1094
  });
968
1095
  }
969
1096
  if (failed) {
@@ -1032,15 +1159,15 @@ async function* _startWebSocketOutputOnFirstEvent(events, output, stream, onStar
1032
1159
  yield event;
1033
1160
  }
1034
1161
  }
1035
- async function processWebSocketStream(url, body, headers, output, stream, model, onStart, onResponseEvent, options) {
1036
- const { socket, entry, reused, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal, options?.env);
1162
+ async function processWebSocketStream(url, body, headers, output, stream, model, onStart, onResponseEvent, cacheSessionId, idleTimeoutMs, websocketConnectTimeoutMs, options) {
1163
+ const { socket, entry, reused, release } = await acquireWebSocket(url, headers, cacheSessionId, options?.signal, options?.env, websocketConnectTimeoutMs);
1037
1164
  let keepConnection = true;
1038
1165
  const useCachedContext = options?.transport === "websocket-cached" || options?.transport === "auto";
1039
1166
  // ChatGPT Codex Responses rejects `store: true` ("Store must be set to false").
1040
1167
  // WebSocket continuation still works via connection-scoped previous_response_id state.
1041
1168
  const fullBody = body;
1042
1169
  const requestBody = useCachedContext && entry ? buildCachedWebSocketRequestBody(entry, fullBody) : fullBody;
1043
- const stats = options?.sessionId ? getOrCreateWebSocketDebugStats(options.sessionId) : undefined;
1170
+ const stats = cacheSessionId ? getOrCreateWebSocketDebugStats(cacheSessionId) : undefined;
1044
1171
  if (stats) {
1045
1172
  stats.requests++;
1046
1173
  if (reused)
@@ -1066,10 +1193,11 @@ async function processWebSocketStream(url, body, headers, output, stream, model,
1066
1193
  try {
1067
1194
  socket.send(JSON.stringify({ type: "response.create", ...requestBody }));
1068
1195
  onStart();
1069
- stream.push({ type: "start", partial: output });
1070
1196
  async function* observedWebSocketEvents() {
1071
- for await (const event of parseWebSocket(socket, options?.signal)) {
1072
- onResponseEvent();
1197
+ for await (const event of parseWebSocket(socket, options?.signal, idleTimeoutMs)) {
1198
+ // A connection-limit rejection carries no response data, so the request can be retried.
1199
+ if (!isWebSocketConnectionLimitReachedEvent(event))
1200
+ onResponseEvent();
1073
1201
  yield event;
1074
1202
  }
1075
1203
  }
@@ -1110,9 +1238,11 @@ async function parseErrorResponse(response) {
1110
1238
  const raw = await response.text();
1111
1239
  let message = raw || response.statusText || "Request failed";
1112
1240
  let friendlyMessage;
1241
+ let errorMetadata;
1113
1242
  try {
1114
1243
  const parsed = JSON.parse(raw);
1115
1244
  const err = parsed?.error;
1245
+ errorMetadata = normalizeProviderError(parsed).errorMetadata;
1116
1246
  if (err) {
1117
1247
  const code = err.code || err.type || "";
1118
1248
  if (/usage_limit_reached|usage_not_included|rate_limit_exceeded/i.test(code) || response.status === 429) {
@@ -1127,7 +1257,7 @@ async function parseErrorResponse(response) {
1127
1257
  }
1128
1258
  }
1129
1259
  catch { }
1130
- return { message, friendlyMessage };
1260
+ return { message, friendlyMessage, errorMetadata };
1131
1261
  }
1132
1262
  // ============================================================================
1133
1263
  // Auth & Headers
@@ -1147,22 +1277,18 @@ function extractAccountId(token) {
1147
1277
  throw new Error("Failed to extract accountId from token");
1148
1278
  }
1149
1279
  }
1150
- function createCodexRequestId() {
1151
- if (typeof globalThis.crypto?.randomUUID === "function") {
1152
- return globalThis.crypto.randomUUID();
1153
- }
1154
- return `codex_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
1155
- }
1156
1280
  function buildBaseCodexHeaders(initHeaders, additionalHeaders, accountId, token) {
1157
- const headers = new Headers(initHeaders);
1281
+ const userAgent = _os ? `pi (${_os.platform()} ${_os.release()}; ${_os.arch()})` : "pi (browser)";
1282
+ // Defaults first so model and caller headers can override them, matching the other providers.
1283
+ const headers = new Headers({ originator: "pi", "User-Agent": userAgent });
1284
+ for (const [key, value] of Object.entries(initHeaders || {})) {
1285
+ headers.set(key, value);
1286
+ }
1158
1287
  for (const [key, value] of Object.entries(additionalHeaders || {})) {
1159
1288
  headers.set(key, value);
1160
1289
  }
1161
1290
  headers.set("Authorization", `Bearer ${token}`);
1162
1291
  headers.set("chatgpt-account-id", accountId);
1163
- headers.set("originator", "pi");
1164
- const userAgent = _os ? `pi (${_os.platform()} ${_os.release()}; ${_os.arch()})` : "pi (browser)";
1165
- headers.set("User-Agent", userAgent);
1166
1292
  return headers;
1167
1293
  }
1168
1294
  function buildSSEHeaders(initHeaders, additionalHeaders, accountId, token, sessionId) {
@@ -1171,7 +1297,7 @@ function buildSSEHeaders(initHeaders, additionalHeaders, accountId, token, sessi
1171
1297
  headers.set("accept", "text/event-stream");
1172
1298
  headers.set("content-type", "application/json");
1173
1299
  if (sessionId) {
1174
- headers.set("session_id", sessionId);
1300
+ headers.set("session-id", sessionId);
1175
1301
  headers.set("x-client-request-id", sessionId);
1176
1302
  }
1177
1303
  return headers;
@@ -1184,7 +1310,7 @@ function buildWebSocketHeaders(initHeaders, additionalHeaders, accountId, token,
1184
1310
  headers.delete("openai-beta");
1185
1311
  headers.set("OpenAI-Beta", OPENAI_BETA_RESPONSES_WEBSOCKETS);
1186
1312
  headers.set("x-client-request-id", requestId);
1187
- headers.set("session_id", requestId);
1313
+ headers.set("session-id", requestId);
1188
1314
  return headers;
1189
1315
  }
1190
1316
  //# sourceMappingURL=openai-codex-responses.js.map