@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.
- package/dist/api-registry.d.ts +1 -1
- package/dist/api-registry.d.ts.map +1 -1
- package/dist/api-registry.js.map +1 -1
- package/dist/bedrock-provider.d.ts +2 -2
- package/dist/bedrock-provider.d.ts.map +1 -1
- package/dist/bedrock-provider.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +14 -0
- package/dist/cli.js.map +1 -1
- package/dist/env-api-keys.d.ts +10 -1
- package/dist/env-api-keys.d.ts.map +1 -1
- package/dist/env-api-keys.js +110 -36
- package/dist/env-api-keys.js.map +1 -1
- package/dist/image-models.d.ts +10 -0
- package/dist/image-models.d.ts.map +1 -0
- package/dist/image-models.generated.d.ts +485 -0
- package/dist/image-models.generated.d.ts.map +1 -0
- package/dist/image-models.generated.js +487 -0
- package/dist/image-models.generated.js.map +1 -0
- package/dist/image-models.js +23 -0
- package/dist/image-models.js.map +1 -0
- package/dist/images-api-registry.d.ts +14 -0
- package/dist/images-api-registry.d.ts.map +1 -0
- package/dist/images-api-registry.js +22 -0
- package/dist/images-api-registry.js.map +1 -0
- package/dist/images.d.ts +4 -0
- package/dist/images.d.ts.map +1 -0
- package/dist/images.js +14 -0
- package/dist/images.js.map +1 -0
- package/dist/index.d.ts +31 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/models.d.ts +5 -8
- package/dist/models.d.ts.map +1 -1
- package/dist/models.generated.d.ts +5197 -1721
- package/dist/models.generated.d.ts.map +1 -1
- package/dist/models.generated.js +7156 -5016
- package/dist/models.generated.js.map +1 -1
- package/dist/models.js +33 -6
- package/dist/models.js.map +1 -1
- package/dist/oauth.d.ts +1 -1
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js.map +1 -1
- package/dist/providers/amazon-bedrock.d.ts +19 -1
- package/dist/providers/amazon-bedrock.d.ts.map +1 -1
- package/dist/providers/amazon-bedrock.js +278 -89
- package/dist/providers/amazon-bedrock.js.map +1 -1
- package/dist/providers/anthropic.d.ts +37 -6
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +300 -114
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/azure-openai-responses.d.ts +1 -1
- package/dist/providers/azure-openai-responses.d.ts.map +1 -1
- package/dist/providers/azure-openai-responses.js +68 -21
- package/dist/providers/azure-openai-responses.js.map +1 -1
- package/dist/providers/cloudflare.d.ts +13 -0
- package/dist/providers/cloudflare.d.ts.map +1 -0
- package/dist/providers/cloudflare.js +26 -0
- package/dist/providers/cloudflare.js.map +1 -0
- package/dist/providers/faux.d.ts +1 -1
- package/dist/providers/faux.d.ts.map +1 -1
- package/dist/providers/faux.js +1 -0
- package/dist/providers/faux.js.map +1 -1
- package/dist/providers/github-copilot-headers.d.ts +1 -1
- package/dist/providers/github-copilot-headers.d.ts.map +1 -1
- package/dist/providers/github-copilot-headers.js.map +1 -1
- package/dist/providers/google-shared.d.ts +8 -3
- package/dist/providers/google-shared.d.ts.map +1 -1
- package/dist/providers/google-shared.js +34 -17
- package/dist/providers/google-shared.js.map +1 -1
- package/dist/providers/google-vertex.d.ts +2 -2
- package/dist/providers/google-vertex.d.ts.map +1 -1
- package/dist/providers/google-vertex.js +45 -18
- package/dist/providers/google-vertex.js.map +1 -1
- package/dist/providers/google.d.ts +2 -2
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +9 -6
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/images/openrouter.d.ts +3 -0
- package/dist/providers/images/openrouter.d.ts.map +1 -0
- package/dist/providers/images/openrouter.js +128 -0
- package/dist/providers/images/openrouter.js.map +1 -0
- package/dist/providers/images/register-builtins.d.ts +4 -0
- package/dist/providers/images/register-builtins.d.ts.map +1 -0
- package/dist/providers/images/register-builtins.js +34 -0
- package/dist/providers/images/register-builtins.js.map +1 -0
- package/dist/providers/mistral.d.ts +4 -1
- package/dist/providers/mistral.d.ts.map +1 -1
- package/dist/providers/mistral.js +43 -10
- package/dist/providers/mistral.js.map +1 -1
- package/dist/providers/openai-codex-responses.d.ts +22 -1
- package/dist/providers/openai-codex-responses.d.ts.map +1 -1
- package/dist/providers/openai-codex-responses.js +542 -111
- package/dist/providers/openai-codex-responses.js.map +1 -1
- package/dist/providers/openai-completions.d.ts +6 -2
- package/dist/providers/openai-completions.d.ts.map +1 -1
- package/dist/providers/openai-completions.js +446 -227
- package/dist/providers/openai-completions.js.map +1 -1
- package/dist/providers/openai-prompt-cache.d.ts +3 -0
- package/dist/providers/openai-prompt-cache.d.ts.map +1 -0
- package/dist/providers/openai-prompt-cache.js +10 -0
- package/dist/providers/openai-prompt-cache.js.map +1 -0
- package/dist/providers/openai-responses-shared.d.ts +3 -2
- package/dist/providers/openai-responses-shared.d.ts.map +1 -1
- package/dist/providers/openai-responses-shared.js +41 -15
- package/dist/providers/openai-responses-shared.js.map +1 -1
- package/dist/providers/openai-responses.d.ts +1 -1
- package/dist/providers/openai-responses.d.ts.map +1 -1
- package/dist/providers/openai-responses.js +85 -40
- package/dist/providers/openai-responses.js.map +1 -1
- package/dist/providers/register-builtins.d.ts +10 -13
- package/dist/providers/register-builtins.d.ts.map +1 -1
- package/dist/providers/register-builtins.js +13 -20
- package/dist/providers/register-builtins.js.map +1 -1
- package/dist/providers/simple-options.d.ts +2 -2
- package/dist/providers/simple-options.d.ts.map +1 -1
- package/dist/providers/simple-options.js +8 -2
- package/dist/providers/simple-options.js.map +1 -1
- package/dist/providers/transform-messages.d.ts +1 -1
- package/dist/providers/transform-messages.d.ts.map +1 -1
- package/dist/providers/transform-messages.js +63 -34
- package/dist/providers/transform-messages.js.map +1 -1
- package/dist/session-resources.d.ts +4 -0
- package/dist/session-resources.d.ts.map +1 -0
- package/dist/session-resources.js +22 -0
- package/dist/session-resources.js.map +1 -0
- package/dist/stream.d.ts +3 -3
- package/dist/stream.d.ts.map +1 -1
- package/dist/stream.js +14 -2
- package/dist/stream.js.map +1 -1
- package/dist/types.d.ts +177 -14
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/abort-signals.d.ts +6 -0
- package/dist/utils/abort-signals.d.ts.map +1 -0
- package/dist/utils/abort-signals.js +34 -0
- package/dist/utils/abort-signals.js.map +1 -0
- package/dist/utils/diagnostics.d.ts +19 -0
- package/dist/utils/diagnostics.d.ts.map +1 -0
- package/dist/utils/diagnostics.js +25 -0
- package/dist/utils/diagnostics.js.map +1 -0
- package/dist/utils/event-stream.d.ts +3 -3
- package/dist/utils/event-stream.d.ts.map +1 -1
- package/dist/utils/event-stream.js +2 -2
- package/dist/utils/event-stream.js.map +1 -1
- package/dist/utils/headers.d.ts +2 -0
- package/dist/utils/headers.d.ts.map +1 -0
- package/dist/utils/headers.js +8 -0
- package/dist/utils/headers.js.map +1 -0
- package/dist/utils/json-parse.d.ts +8 -1
- package/dist/utils/json-parse.d.ts.map +1 -1
- package/dist/utils/json-parse.js +89 -5
- package/dist/utils/json-parse.js.map +1 -1
- package/dist/utils/node-http-proxy.d.ts +10 -0
- package/dist/utils/node-http-proxy.d.ts.map +1 -0
- package/dist/utils/node-http-proxy.js +97 -0
- package/dist/utils/node-http-proxy.js.map +1 -0
- package/dist/utils/oauth/anthropic.d.ts +1 -1
- package/dist/utils/oauth/anthropic.d.ts.map +1 -1
- package/dist/utils/oauth/anthropic.js +1 -1
- package/dist/utils/oauth/anthropic.js.map +1 -1
- package/dist/utils/oauth/device-code.d.ts +21 -0
- package/dist/utils/oauth/device-code.d.ts.map +1 -0
- package/dist/utils/oauth/device-code.js +56 -0
- package/dist/utils/oauth/device-code.js.map +1 -0
- package/dist/utils/oauth/github-copilot.d.ts +3 -3
- package/dist/utils/oauth/github-copilot.d.ts.map +1 -1
- package/dist/utils/oauth/github-copilot.js +58 -70
- package/dist/utils/oauth/github-copilot.js.map +1 -1
- package/dist/utils/oauth/index.d.ts +8 -11
- package/dist/utils/oauth/index.d.ts.map +1 -1
- package/dist/utils/oauth/index.js +2 -11
- package/dist/utils/oauth/index.js.map +1 -1
- package/dist/utils/oauth/openai-codex.d.ts +11 -2
- package/dist/utils/oauth/openai-codex.d.ts.map +1 -1
- package/dist/utils/oauth/openai-codex.js +187 -73
- package/dist/utils/oauth/openai-codex.js.map +1 -1
- package/dist/utils/oauth/types.d.ts +18 -1
- package/dist/utils/oauth/types.d.ts.map +1 -1
- package/dist/utils/oauth/types.js.map +1 -1
- package/dist/utils/overflow.d.ts +7 -3
- package/dist/utils/overflow.d.ts.map +1 -1
- package/dist/utils/overflow.js +25 -3
- package/dist/utils/overflow.js.map +1 -1
- package/dist/utils/typebox-helpers.d.ts +1 -1
- package/dist/utils/typebox-helpers.d.ts.map +1 -1
- package/dist/utils/typebox-helpers.js +1 -1
- package/dist/utils/typebox-helpers.js.map +1 -1
- package/dist/utils/validation.d.ts +1 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +242 -41
- package/dist/utils/validation.js.map +1 -1
- package/package.json +15 -16
- package/dist/providers/google-gemini-cli.d.ts +0 -74
- package/dist/providers/google-gemini-cli.d.ts.map +0 -1
- package/dist/providers/google-gemini-cli.js +0 -776
- package/dist/providers/google-gemini-cli.js.map +0 -1
|
@@ -1,25 +1,41 @@
|
|
|
1
|
-
|
|
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 {
|
|
11
|
-
import {
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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 <
|
|
134
|
-
const
|
|
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 <
|
|
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
|
|
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
|
|
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 || "
|
|
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 (
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
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
|
|
275
|
-
|
|
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
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
|
403
|
-
if (
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
|
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
|
|
418
|
-
const error = extractWebSocketCloseError(event);
|
|
709
|
+
const onOpen = () => {
|
|
419
710
|
if (settled)
|
|
420
711
|
return;
|
|
421
712
|
settled = true;
|
|
422
713
|
cleanup();
|
|
423
|
-
|
|
714
|
+
resolve(socket);
|
|
424
715
|
};
|
|
425
|
-
const
|
|
426
|
-
|
|
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
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
451
|
-
|
|
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"
|
|
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
|
-
|
|
529
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
639
|
-
const {
|
|
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", ...
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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", "
|
|
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("
|
|
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("
|
|
1169
|
+
headers.set("session-id", requestId);
|
|
739
1170
|
return headers;
|
|
740
1171
|
}
|
|
741
1172
|
//# sourceMappingURL=openai-codex-responses.js.map
|