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