openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
979 lines (978 loc) • 36.9 kB
JavaScript
import { f as clampTimerTimeoutMs, j as resolveTimerTimeoutMs } from "./number-coercion-CJQ8TR--.js";
import { a as formatThrownValue, i as createAssistantMessageDiagnostic, r as appendAssistantMessageDiagnostic } from "./src-M7TBQdDX.js";
import { t as AssistantMessageEventStream } from "./event-stream-ReMmOTzX.js";
import { s as buildBaseOptions, u as clampThinkingLevel } from "./sanitize-unicode-CgzZKr22.js";
import { n as registerSessionResourceCleanup } from "./session-resources-CV5NrnlT.js";
import { n as getEnvApiKey } from "./env-api-keys-8q9bEA0v.js";
import { t as headersToRecord } from "./headers-CaXpIDsu.js";
import { t as clampOpenAIPromptCacheKey } from "./openai-prompt-cache-Bx1f-UHU.js";
import { i as processResponsesStream, n as convertResponsesMessages, s as convertResponsesTools } from "./openai-responses-shared-C7frbROV.js";
//#region src/llm/utils/oauth/openai-chatgpt-jwt.ts
const OPENAI_CODEX_AUTH_CLAIM = "https://api.openai.com/auth";
function decodeOpenAICodexJwtPayload(token) {
const parts = token.split(".");
if (parts.length !== 3) return null;
try {
const decoded = Buffer.from(parts[1] ?? "", "base64url").toString("utf8");
const parsed = JSON.parse(decoded);
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
} catch {
return null;
}
}
function resolveOpenAICodexAccountId(token) {
const accountId = decodeOpenAICodexJwtPayload(token)?.[OPENAI_CODEX_AUTH_CLAIM]?.chatgpt_account_id;
return typeof accountId === "string" && accountId.length > 0 ? accountId : null;
}
//#endregion
//#region src/llm/providers/openai-chatgpt-responses.ts
let os = null;
const dynamicImport = (specifier) => import(specifier);
const NODE_OS_SPECIFIER = "node:os";
if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) dynamicImport(NODE_OS_SPECIFIER).then((m) => {
os = m;
});
const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
const MAX_RETRIES = 3;
const BASE_DELAY_MS = 1e3;
const RETRY_AFTER_HTTP_DATE_RE = /^(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), \d{2}-(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-\d{2} \d{2}:\d{2}:\d{2} GMT|(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [ \d]\d \d{2}:\d{2}:\d{2} \d{4})$/;
const CODEX_TOOL_CALL_PROVIDERS = new Set(["openai", "opencode"]);
const WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE = 1009;
const CODEX_RESPONSE_STATUSES = new Set([
"completed",
"incomplete",
"failed",
"cancelled",
"queued",
"in_progress"
]);
function isRetryableError(status, errorText) {
if (status === 429 || status === 500 || status === 502 || status === 503 || status === 504) return true;
return /rate.?limit|overloaded|service.?unavailable|upstream.?connect|connection.?refused/i.test(errorText);
}
function sleep(ms, signal) {
return new Promise((resolve, reject) => {
if (signal?.aborted) {
reject(/* @__PURE__ */ new Error("Request was aborted"));
return;
}
const timeout = setTimeout(resolve, ms);
signal?.addEventListener("abort", () => {
clearTimeout(timeout);
reject(/* @__PURE__ */ new Error("Request was aborted"));
});
});
}
function resolveRequestTimeoutMs(options) {
const timeoutMs = options?.timeoutMs;
return typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? resolveTimerTimeoutMs(timeoutMs, 1) : void 0;
}
function buildRequestSignal(baseSignal, timeoutMs) {
if (timeoutMs === void 0) return baseSignal;
const timeoutSignal = AbortSignal.timeout(timeoutMs);
if (!baseSignal) return timeoutSignal;
return AbortSignal.any([baseSignal, timeoutSignal]);
}
function isRequestTimeoutError(error, callerSignal, requestSignal, timeoutMs) {
if (timeoutMs === void 0 || callerSignal?.aborted || !requestSignal?.aborted) return false;
if (!(error instanceof Error)) return false;
return error.name === "AbortError" || error.name === "TimeoutError" || error.message === "Request was aborted";
}
function formatRequestTimeoutError(timeoutMs, cause) {
return new Error(`Request timed out after ${timeoutMs}ms`, { cause: cause instanceof Error ? cause : void 0 });
}
const streamOpenAICodexResponses = (model, context, options) => {
const stream = new AssistantMessageEventStream();
(async () => {
let requestTimeoutMs;
let activeSignal;
const output = {
role: "assistant",
content: [],
api: "openai-chatgpt-responses",
provider: model.provider,
model: model.id,
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0
}
},
stopReason: "stop",
timestamp: Date.now()
};
try {
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
if (!apiKey) throw new Error(`No API key for provider: ${model.provider}`);
const accountId = extractOpenAICodexAccountId(apiKey);
let body = buildRequestBody(model, context, options);
const nextBody = await options?.onPayload?.(body, model);
if (nextBody !== void 0) body = nextBody;
const websocketRequestId = options?.sessionId || createCodexRequestId();
const sseHeaders = buildSSEHeaders(model.headers, options?.headers, accountId, apiKey, options?.sessionId);
const websocketHeaders = buildWebSocketHeaders(model.headers, options?.headers, accountId, apiKey, websocketRequestId);
const bodyJson = JSON.stringify(body);
requestTimeoutMs = resolveRequestTimeoutMs(options);
activeSignal = buildRequestSignal(options?.signal, requestTimeoutMs);
const requestOptions = activeSignal === options?.signal ? options : {
...options,
signal: activeSignal
};
const transport = options?.transport || "auto";
const websocketDisabledForSession = transport === "auto" && isWebSocketSseFallbackActive(options?.sessionId);
if (websocketDisabledForSession) recordWebSocketSseFallback(options?.sessionId);
if (transport !== "sse" && !websocketDisabledForSession) {
let websocketStarted = false;
try {
await processWebSocketStream(resolveCodexWebSocketUrl(model.baseUrl), body, websocketHeaders, output, stream, model, () => {
websocketStarted = true;
}, requestOptions);
if (activeSignal?.aborted) throw new Error("Request was aborted");
stream.push({
type: "done",
reason: output.stopReason,
message: output
});
stream.end();
return;
} catch (error) {
if (activeSignal?.aborted || isCodexNonTransportError(error)) throw error;
appendAssistantMessageDiagnostic(output, createAssistantMessageDiagnostic("provider_transport_failure", error, {
configuredTransport: transport,
fallbackTransport: transport === "auto" && !websocketStarted ? "sse" : void 0,
eventsEmitted: websocketStarted,
phase: websocketStarted ? "after_message_stream_start" : "before_message_stream_start",
requestBytes: new TextEncoder().encode(bodyJson).byteLength
}));
recordWebSocketFailure(options?.sessionId, error, { activateSseFallback: transport === "auto" });
if (websocketStarted || transport !== "auto") throw error;
recordWebSocketSseFallback(options?.sessionId);
}
}
let response;
let lastError;
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
if (activeSignal?.aborted) throw new Error("Request was aborted");
try {
response = await fetch(resolveCodexUrl(model.baseUrl), {
method: "POST",
headers: sseHeaders,
body: bodyJson,
signal: activeSignal
});
await options?.onResponse?.({
status: response.status,
headers: headersToRecord(response.headers)
}, model);
if (response.ok) break;
const errorText = await response.text();
if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
let delayMs = BASE_DELAY_MS * 2 ** attempt;
const retryAfterMs = response.headers.get("retry-after-ms");
if (retryAfterMs !== null) {
const trimmedRetryAfterMs = retryAfterMs.trim();
const millis = Number(trimmedRetryAfterMs);
if (/^\d+(?:\.\d+)?$/.test(trimmedRetryAfterMs) && Number.isFinite(millis)) delayMs = clampTimerTimeoutMs(millis, 0) ?? delayMs;
} else {
const retryAfter = response.headers.get("retry-after");
if (retryAfter) {
const trimmedRetryAfter = retryAfter.trim();
const seconds = Number(trimmedRetryAfter);
if (/^\d+$/.test(trimmedRetryAfter) && Number.isFinite(seconds)) delayMs = clampTimerTimeoutMs(seconds * 1e3, 0) ?? delayMs;
else if (RETRY_AFTER_HTTP_DATE_RE.test(trimmedRetryAfter)) {
const date = Date.parse(trimmedRetryAfter);
if (!Number.isNaN(date)) delayMs = clampTimerTimeoutMs(date - Date.now(), 0) ?? delayMs;
}
}
}
await sleep(delayMs, activeSignal);
continue;
}
const info = await parseErrorResponse(new Response(errorText, {
status: response.status,
statusText: response.statusText
}));
throw new Error(info.friendlyMessage || info.message);
} catch (error) {
if (error instanceof Error) {
if (isRequestTimeoutError(error, options?.signal, activeSignal, requestTimeoutMs) && requestTimeoutMs !== void 0) throw formatRequestTimeoutError(requestTimeoutMs, error);
if (error.name === "AbortError" || error.message === "Request was aborted") throw new Error("Request was aborted", { cause: error });
if (error.name === "TimeoutError" && requestTimeoutMs !== void 0) throw new Error(`Request timed out after ${requestTimeoutMs}ms`, { cause: error });
}
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt < MAX_RETRIES && !lastError.message.includes("usage limit")) {
await sleep(BASE_DELAY_MS * 2 ** attempt, activeSignal);
continue;
}
throw lastError;
}
}
if (!response?.ok) throw lastError ?? /* @__PURE__ */ new Error("Failed after retries");
if (!response.body) throw new Error("No response body");
stream.push({
type: "start",
partial: output
});
await processStream(response, output, stream, model, options);
if (activeSignal?.aborted) throw new Error("Request was aborted");
stream.push({
type: "done",
reason: output.stopReason,
message: output
});
stream.end();
} catch (error) {
const normalizedError = isRequestTimeoutError(error, options?.signal, activeSignal, requestTimeoutMs) && requestTimeoutMs !== void 0 ? formatRequestTimeoutError(requestTimeoutMs, error) : error;
for (const block of output.content) delete block.partialJson;
output.stopReason = options?.signal?.aborted ? "aborted" : "error";
output.errorMessage = normalizedError instanceof Error ? normalizedError.message : String(normalizedError);
stream.push({
type: "error",
reason: output.stopReason,
error: output
});
stream.end();
}
})();
return stream;
};
const streamSimpleOpenAICodexResponses = (model, context, options) => {
const apiKey = options?.apiKey || getEnvApiKey(model.provider);
if (!apiKey) throw new Error(`No API key for provider: ${model.provider}`);
const base = buildBaseOptions(model, options, apiKey);
const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : void 0;
const reasoningEffort = clampedReasoning === "off" ? void 0 : clampedReasoning === "max" ? "xhigh" : clampedReasoning;
return streamOpenAICodexResponses(model, context, {
...base,
reasoningEffort
});
};
function buildRequestBody(model, context, options) {
const messages = convertResponsesMessages(model, context, CODEX_TOOL_CALL_PROVIDERS, {
includeSystemPrompt: false,
replayResponsesItemIds: false
});
const body = {
model: model.id,
store: false,
stream: true,
instructions: context.systemPrompt || "You are a helpful assistant.",
input: messages,
text: { verbosity: options?.textVerbosity || "low" },
include: ["reasoning.encrypted_content"],
prompt_cache_key: options?.cacheRetention === "none" ? void 0 : clampOpenAIPromptCacheKey(options?.promptCacheKey ?? options?.sessionId),
tool_choice: "auto",
parallel_tool_calls: true
};
if (options?.temperature !== void 0) body.temperature = options.temperature;
if (options?.serviceTier !== void 0) body.service_tier = options.serviceTier;
if (context.tools && context.tools.length > 0) body.tools = convertResponsesTools(context.tools, { strict: null });
if (options?.reasoningEffort !== void 0) {
const effort = options.reasoningEffort === "none" ? model.thinkingLevelMap?.off ?? "none" : model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort;
if (effort !== null) body.reasoning = {
effort,
summary: options.reasoningSummary ?? "auto"
};
}
return body;
}
function getServiceTierCostMultiplier(model, serviceTier) {
switch (serviceTier) {
case "flex": return .5;
case "priority": return model.id === "gpt-5.5" ? 2.5 : 2;
default: return 1;
}
}
function applyServiceTierPricing(usage, serviceTier, model) {
const multiplier = getServiceTierCostMultiplier(model, serviceTier);
if (multiplier === 1) return;
usage.cost.input *= multiplier;
usage.cost.output *= multiplier;
usage.cost.cacheRead *= multiplier;
usage.cost.cacheWrite *= multiplier;
usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
}
function resolveCodexServiceTier(responseServiceTier, requestServiceTier) {
if (responseServiceTier === "default" && (requestServiceTier === "flex" || requestServiceTier === "priority")) return requestServiceTier;
return responseServiceTier ?? requestServiceTier;
}
function resolveCodexUrl(baseUrl) {
const normalized = (baseUrl && baseUrl.trim().length > 0 ? baseUrl : DEFAULT_CODEX_BASE_URL).replace(/\/+$/, "");
if (normalized.endsWith("/codex/responses")) return normalized;
if (normalized.endsWith("/codex")) return `${normalized}/responses`;
return `${normalized}/codex/responses`;
}
function resolveCodexWebSocketUrl(baseUrl) {
const url = new URL(resolveCodexUrl(baseUrl));
if (url.protocol === "https:") url.protocol = "wss:";
if (url.protocol === "http:") url.protocol = "ws:";
return url.toString();
}
async function processStream(response, output, stream, model, options) {
await processResponsesStream(mapCodexEvents(parseSSE(response)), output, stream, model, {
serviceTier: options?.serviceTier,
resolveServiceTier: resolveCodexServiceTier,
applyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model)
});
}
var CodexApiError = class extends Error {
constructor(message, options) {
super(message);
this.name = "CodexApiError";
this.code = options?.code;
this.payload = options?.payload;
this.cause = options?.cause;
}
};
var CodexProtocolError = class extends Error {
constructor(message, options) {
super(message);
this.name = "CodexProtocolError";
this.payload = options?.payload;
this.cause = options?.cause;
}
};
function isCodexNonTransportError(error) {
return error instanceof CodexApiError || error instanceof CodexProtocolError;
}
async function* mapCodexEvents(events) {
for await (const event of events) {
const type = typeof event.type === "string" ? event.type : void 0;
if (!type) continue;
if (type === "error") {
const code = event.code || "";
throw new CodexApiError(`Codex error: ${event.message || code || JSON.stringify(event)}`, {
code: code || void 0,
payload: event
});
}
if (type === "response.failed") {
const response = event.response;
const code = response?.error?.code;
const message = response?.error?.message;
throw new CodexApiError(message || "Codex response failed", {
code,
payload: event
});
}
if (type === "response.done" || type === "response.completed" || type === "response.incomplete") {
const response = event.response;
const normalizedResponse = response ? {
...response,
status: normalizeCodexStatus(response.status)
} : response;
yield {
...event,
type: "response.completed",
response: normalizedResponse
};
return;
}
yield event;
}
}
function normalizeCodexStatus(status) {
if (typeof status !== "string") return;
return CODEX_RESPONSE_STATUSES.has(status) ? status : void 0;
}
async function* parseSSE(response) {
if (!response.body) return;
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let idx = buffer.indexOf("\n\n");
while (idx !== -1) {
const chunk = buffer.slice(0, idx);
buffer = buffer.slice(idx + 2);
const dataLines = chunk.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim());
if (dataLines.length > 0) {
const data = dataLines.join("\n").trim();
if (data && data !== "[DONE]") try {
yield JSON.parse(data);
} catch (cause) {
throw new CodexProtocolError(`Invalid Codex SSE JSON: ${formatThrownValue(cause)}`, {
cause,
payload: data
});
}
}
idx = buffer.indexOf("\n\n");
}
}
} finally {
try {
await reader.cancel();
} catch {}
try {
reader.releaseLock();
} catch {}
}
}
const OPENAI_BETA_RESPONSES_WEBSOCKETS = "responses_websockets=2026-02-06";
const SESSION_WEBSOCKET_CACHE_TTL_MS = 300 * 1e3;
const websocketSessionCache = /* @__PURE__ */ new Map();
const websocketDebugStats = /* @__PURE__ */ new Map();
const websocketSseFallbackSessions = /* @__PURE__ */ new Set();
function getOrCreateWebSocketDebugStats(sessionId) {
let stats = websocketDebugStats.get(sessionId);
if (!stats) {
stats = {
requests: 0,
connectionsCreated: 0,
connectionsReused: 0,
cachedContextRequests: 0,
storeTrueRequests: 0,
fullContextRequests: 0,
deltaRequests: 0,
lastInputItems: 0,
websocketFailures: 0,
sseFallbacks: 0
};
websocketDebugStats.set(sessionId, stats);
}
return stats;
}
function getOpenAICodexWebSocketDebugStats(sessionId) {
const stats = websocketDebugStats.get(sessionId);
return stats ? { ...stats } : void 0;
}
function resetOpenAICodexWebSocketDebugStats(sessionId) {
if (sessionId) {
websocketDebugStats.delete(sessionId);
websocketSseFallbackSessions.delete(sessionId);
return;
}
websocketDebugStats.clear();
websocketSseFallbackSessions.clear();
}
function closeOpenAICodexWebSocketSessions(sessionId) {
const closeEntry = (entry) => {
if (entry.idleTimer) clearTimeout(entry.idleTimer);
closeWebSocketSilently(entry.socket, 1e3, "debug_close");
};
if (sessionId) {
const entry = websocketSessionCache.get(sessionId);
if (entry) closeEntry(entry);
websocketSessionCache.delete(sessionId);
return;
}
for (const entry of websocketSessionCache.values()) closeEntry(entry);
websocketSessionCache.clear();
}
registerSessionResourceCleanup(closeOpenAICodexWebSocketSessions);
function isWebSocketSseFallbackActive(sessionId) {
return sessionId ? websocketSseFallbackSessions.has(sessionId) : false;
}
function recordWebSocketSseFallback(sessionId) {
if (!sessionId) return;
const stats = getOrCreateWebSocketDebugStats(sessionId);
stats.sseFallbacks++;
stats.websocketFallbackActive = isWebSocketSseFallbackActive(sessionId);
}
function recordWebSocketFailure(sessionId, error, options) {
if (!sessionId) return;
if (options.activateSseFallback) websocketSseFallbackSessions.add(sessionId);
const stats = getOrCreateWebSocketDebugStats(sessionId);
stats.websocketFailures++;
stats.lastWebSocketError = formatThrownValue(error);
stats.websocketFallbackActive = isWebSocketSseFallbackActive(sessionId);
}
let cachedWebsocket = null;
async function getWebSocketConstructor() {
if (cachedWebsocket) return cachedWebsocket;
if (process?.versions?.bun && (process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy)) {
const getProxyForUrl = (await dynamicImport("proxy-from-env")).getProxyForUrl;
cachedWebsocket = class extends WebSocket {
constructor(url, options) {
let opts;
if (Array.isArray(options) || typeof options === "string") opts = { protocols: options };
else opts = { ...options };
const proxy = getProxyForUrl(url.toString().replace(/^wss:/, "https:").replace(/^ws:/, "http:"));
super(url, {
...opts,
...proxy ? { proxy } : {}
});
}
};
return cachedWebsocket;
}
const ctor = globalThis.WebSocket;
if (typeof ctor !== "function") return null;
return ctor;
}
var WebSocketCloseError = class extends Error {
constructor(message, options) {
super(message);
this.name = "WebSocketCloseError";
this.code = options?.code;
this.reason = options?.reason;
this.wasClean = options?.wasClean;
}
};
function getWebSocketReadyState(socket) {
const readyState = socket.readyState;
return typeof readyState === "number" ? readyState : void 0;
}
function isWebSocketReusable(socket) {
const readyState = getWebSocketReadyState(socket);
return readyState === void 0 || readyState === 1;
}
function closeWebSocketSilently(socket, code = 1e3, reason = "done") {
try {
socket.close(code, reason);
} catch {}
}
function scheduleSessionWebSocketExpiry(sessionId, entry) {
if (entry.idleTimer) clearTimeout(entry.idleTimer);
entry.idleTimer = setTimeout(() => {
if (entry.busy) return;
closeWebSocketSilently(entry.socket, 1e3, "idle_timeout");
websocketSessionCache.delete(sessionId);
}, SESSION_WEBSOCKET_CACHE_TTL_MS);
}
async function connectWebSocket(url, headers, signal) {
const WebSocketCtor = await getWebSocketConstructor();
if (!WebSocketCtor) throw new Error("WebSocket transport is not available in this runtime");
const wsHeaders = headersToRecord(headers);
delete wsHeaders["OpenAI-Beta"];
return new Promise((resolve, reject) => {
let settled = false;
let socket;
try {
socket = new WebSocketCtor(url, { headers: wsHeaders });
} catch (error) {
reject(error instanceof Error ? error : new Error(String(error)));
return;
}
const onOpen = () => {
if (settled) return;
settled = true;
cleanup();
resolve(socket);
};
const onError = (event) => {
const error = extractWebSocketError(event);
if (settled) return;
settled = true;
cleanup();
reject(error);
};
const onClose = (event) => {
const error = extractWebSocketCloseError(event);
if (settled) return;
settled = true;
cleanup();
reject(error);
};
const onAbort = () => {
if (settled) return;
settled = true;
cleanup();
socket.close(1e3, "aborted");
reject(/* @__PURE__ */ new Error("Request was aborted"));
};
const cleanup = () => {
socket.removeEventListener("open", onOpen);
socket.removeEventListener("error", onError);
socket.removeEventListener("close", onClose);
signal?.removeEventListener("abort", onAbort);
};
if (signal?.aborted) {
onAbort();
return;
}
socket.addEventListener("open", onOpen);
socket.addEventListener("error", onError);
socket.addEventListener("close", onClose);
signal?.addEventListener("abort", onAbort);
});
}
async function acquireWebSocket(url, headers, sessionId, signal) {
if (!sessionId) {
const socket = await connectWebSocket(url, headers, signal);
return {
socket,
reused: false,
release: ({ keep } = {}) => {
if (keep === false) {
closeWebSocketSilently(socket);
return;
}
closeWebSocketSilently(socket);
}
};
}
const cached = websocketSessionCache.get(sessionId);
if (cached) {
if (cached.idleTimer) {
clearTimeout(cached.idleTimer);
cached.idleTimer = void 0;
}
if (!cached.busy && isWebSocketReusable(cached.socket)) {
cached.busy = true;
return {
socket: cached.socket,
entry: cached,
reused: true,
release: ({ keep } = {}) => {
if (!keep || !isWebSocketReusable(cached.socket)) {
closeWebSocketSilently(cached.socket);
websocketSessionCache.delete(sessionId);
return;
}
cached.busy = false;
scheduleSessionWebSocketExpiry(sessionId, cached);
}
};
}
if (cached.busy) {
const socket = await connectWebSocket(url, headers, signal);
return {
socket,
reused: false,
release: () => {
closeWebSocketSilently(socket);
}
};
}
if (!isWebSocketReusable(cached.socket)) {
closeWebSocketSilently(cached.socket);
websocketSessionCache.delete(sessionId);
}
}
const socket = await connectWebSocket(url, headers, signal);
const entry = {
socket,
busy: true
};
websocketSessionCache.set(sessionId, entry);
return {
socket,
entry,
reused: false,
release: ({ keep } = {}) => {
if (!keep || !isWebSocketReusable(entry.socket)) {
closeWebSocketSilently(entry.socket);
if (entry.idleTimer) clearTimeout(entry.idleTimer);
if (websocketSessionCache.get(sessionId) === entry) websocketSessionCache.delete(sessionId);
return;
}
entry.busy = false;
scheduleSessionWebSocketExpiry(sessionId, entry);
}
};
}
function extractWebSocketError(event) {
if (event && typeof event === "object") {
const message = "message" in event ? event.message : void 0;
if (typeof message === "string" && message.length > 0) return new Error(message);
const nestedError = "error" in event ? event.error : void 0;
if (nestedError instanceof Error && nestedError.message.length > 0) return nestedError;
if (nestedError && typeof nestedError === "object" && "message" in nestedError) {
const nestedMessage = nestedError.message;
if (typeof nestedMessage === "string" && nestedMessage.length > 0) return new Error(nestedMessage);
}
}
return /* @__PURE__ */ new Error("WebSocket error");
}
function extractWebSocketCloseError(event) {
if (event && typeof event === "object") {
const code = "code" in event ? event.code : void 0;
const reason = "reason" in event ? event.reason : void 0;
const wasClean = "wasClean" in event ? event.wasClean : void 0;
const codeText = typeof code === "number" ? ` ${code}` : "";
let reasonText = typeof reason === "string" && reason.length > 0 ? ` ${reason}` : "";
if (!reasonText && code === WEBSOCKET_MESSAGE_TOO_BIG_CLOSE_CODE) reasonText = " message too big";
return new WebSocketCloseError(`WebSocket closed${codeText}${reasonText}`.trim(), {
code: typeof code === "number" ? code : void 0,
reason: typeof reason === "string" && reason.length > 0 ? reason : void 0,
wasClean: typeof wasClean === "boolean" ? wasClean : void 0
});
}
return /* @__PURE__ */ new Error("WebSocket closed");
}
async function decodeWebSocketData(data) {
if (typeof data === "string") return data;
if (data instanceof ArrayBuffer) return new TextDecoder().decode(new Uint8Array(data));
if (ArrayBuffer.isView(data)) {
const view = data;
return new TextDecoder().decode(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
}
if (data && typeof data === "object" && "arrayBuffer" in data) {
const arrayBuffer = await data.arrayBuffer();
return new TextDecoder().decode(new Uint8Array(arrayBuffer));
}
return null;
}
async function* parseWebSocket(socket, signal) {
const queue = [];
let pending = null;
let done = false;
let failed = null;
let sawCompletion = false;
const wake = () => {
if (!pending) return;
const resolve = pending;
pending = null;
resolve();
};
const onMessage = (event) => {
(async () => {
let text = null;
try {
if (!event || typeof event !== "object" || !("data" in event)) return;
text = await decodeWebSocketData(event.data);
if (!text) return;
const parsed = JSON.parse(text);
const type = typeof parsed.type === "string" ? parsed.type : "";
if (type === "response.completed" || type === "response.done" || type === "response.incomplete") {
sawCompletion = true;
done = true;
}
queue.push(parsed);
wake();
} catch (cause) {
failed = new CodexProtocolError(`Invalid Codex WebSocket JSON: ${formatThrownValue(cause)}`, {
cause,
payload: text
});
done = true;
wake();
}
})();
};
const onError = (event) => {
failed = extractWebSocketError(event);
done = true;
wake();
};
const onClose = (event) => {
if (sawCompletion) {
done = true;
wake();
return;
}
if (!failed) failed = extractWebSocketCloseError(event);
done = true;
wake();
};
const onAbort = () => {
failed = /* @__PURE__ */ new Error("Request was aborted");
done = true;
wake();
};
socket.addEventListener("message", onMessage);
socket.addEventListener("error", onError);
socket.addEventListener("close", onClose);
signal?.addEventListener("abort", onAbort);
try {
while (true) {
if (signal?.aborted) throw new Error("Request was aborted");
if (queue.length > 0) {
yield queue.shift();
continue;
}
if (done) break;
await new Promise((resolve) => {
pending = resolve;
});
}
if (failed) throw toLintErrorObject(failed, "Non-Error thrown");
if (!sawCompletion) throw new Error("WebSocket stream closed before response.completed");
} finally {
socket.removeEventListener("message", onMessage);
socket.removeEventListener("error", onError);
socket.removeEventListener("close", onClose);
signal?.removeEventListener("abort", onAbort);
}
}
function requestBodyWithoutInput(body) {
const { input: _input, previous_response_id: _previousResponseId, ...rest } = body;
return rest;
}
function responseInputsEqual(a, b) {
return JSON.stringify(a ?? []) === JSON.stringify(b ?? []);
}
function requestBodiesMatchExceptInput(a, b) {
return JSON.stringify(requestBodyWithoutInput(a)) === JSON.stringify(requestBodyWithoutInput(b));
}
function getCachedWebSocketInputDelta(body, continuation) {
if (!requestBodiesMatchExceptInput(body, continuation.lastRequestBody)) return;
const currentInput = body.input ?? [];
const baseline = [...continuation.lastRequestBody.input ?? [], ...continuation.lastResponseItems];
if (currentInput.length < baseline.length) return;
if (!responseInputsEqual(currentInput.slice(0, baseline.length), baseline)) return;
return currentInput.slice(baseline.length);
}
function buildCachedWebSocketRequestBody(entry, body) {
const continuation = entry.continuation;
if (!continuation) return body;
const delta = getCachedWebSocketInputDelta(body, continuation);
if (!delta || !continuation.lastResponseId) {
entry.continuation = void 0;
return body;
}
return {
...body,
previous_response_id: continuation.lastResponseId,
input: delta
};
}
async function* startWebSocketOutputOnFirstEvent(events, output, stream, onStart) {
let started = false;
for await (const event of events) {
if (!started) {
started = true;
onStart();
stream.push({
type: "start",
partial: output
});
}
yield event;
}
}
async function processWebSocketStream(url, body, headers, output, stream, model, onStart, options) {
const { socket, entry, reused, release } = await acquireWebSocket(url, headers, options?.sessionId, options?.signal);
let keepConnection = true;
const useCachedContext = options?.transport === "websocket-cached" || options?.transport === "auto";
const fullBody = body;
const requestBody = useCachedContext && entry ? buildCachedWebSocketRequestBody(entry, fullBody) : fullBody;
const stats = options?.sessionId ? getOrCreateWebSocketDebugStats(options.sessionId) : void 0;
if (stats) {
stats.requests++;
if (reused) stats.connectionsReused++;
else stats.connectionsCreated++;
if (useCachedContext) stats.cachedContextRequests++;
if (requestBody.store === true) stats.storeTrueRequests++;
stats.lastInputItems = requestBody.input?.length ?? 0;
if (requestBody.previous_response_id) {
stats.deltaRequests++;
stats.lastDeltaInputItems = requestBody.input?.length ?? 0;
stats.lastPreviousResponseId = requestBody.previous_response_id;
} else {
stats.fullContextRequests++;
stats.lastDeltaInputItems = void 0;
stats.lastPreviousResponseId = void 0;
}
}
try {
if (options?.signal?.aborted) throw new Error("Request was aborted");
socket.send(JSON.stringify({
type: "response.create",
...requestBody
}));
await processResponsesStream(startWebSocketOutputOnFirstEvent(mapCodexEvents(parseWebSocket(socket, options?.signal)), output, stream, onStart), output, stream, model, {
serviceTier: options?.serviceTier,
resolveServiceTier: resolveCodexServiceTier,
applyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model)
});
if (options?.signal?.aborted) keepConnection = false;
else if (useCachedContext && entry && output.responseId) {
const responseItems = convertResponsesMessages(model, { messages: [output] }, CODEX_TOOL_CALL_PROVIDERS, {
includeSystemPrompt: false,
replayResponsesItemIds: false
}).filter((item) => item.type !== "function_call_output");
entry.continuation = {
lastRequestBody: fullBody,
lastResponseId: output.responseId,
lastResponseItems: responseItems
};
}
} catch (error) {
if (entry) entry.continuation = void 0;
keepConnection = false;
throw error;
} finally {
release({ keep: keepConnection });
}
}
async function parseErrorResponse(response) {
const raw = await response.text();
let message = raw || response.statusText || "Request failed";
let friendlyMessage;
try {
const err = JSON.parse(raw)?.error;
if (err) {
const code = err.code || err.type || "";
if (/usage_limit_reached|usage_not_included|rate_limit_exceeded/i.test(code) || response.status === 429) {
const plan = err.plan_type ? ` (${err.plan_type.toLowerCase()} plan)` : "";
const mins = err.resets_at ? Math.max(0, Math.round((err.resets_at * 1e3 - Date.now()) / 6e4)) : void 0;
friendlyMessage = `You have hit your ChatGPT usage limit${plan}.${mins !== void 0 ? ` Try again in ~${mins} min.` : ""}`.trim();
}
message = err.message || friendlyMessage || message;
}
} catch {}
return {
message,
friendlyMessage
};
}
function extractOpenAICodexAccountId(token) {
const accountId = resolveOpenAICodexAccountId(token);
if (accountId) return accountId;
throw new Error("Failed to extract accountId from token");
}
function createCodexRequestId() {
const crypto = globalThis.crypto;
if (typeof crypto?.randomUUID === "function") return crypto.randomUUID();
if (typeof crypto?.getRandomValues === "function") {
const bytes = crypto.getRandomValues(new Uint8Array(16));
return `codex_${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
}
throw new Error("Secure random request id generation is unavailable");
}
function buildBaseCodexHeaders(initHeaders, additionalHeaders, accountId, token) {
const headers = new Headers(initHeaders);
for (const [key, value] of Object.entries(additionalHeaders || {})) headers.set(key, value);
headers.set("Authorization", `Bearer ${token}`);
headers.set("chatgpt-account-id", accountId);
headers.set("originator", "openclaw");
const userAgent = os ? `openclaw (${os.platform()} ${os.release()}; ${os.arch()})` : "openclaw (browser)";
headers.set("User-Agent", userAgent);
return headers;
}
function buildSSEHeaders(initHeaders, additionalHeaders, accountId, token, sessionId) {
const headers = buildBaseCodexHeaders(initHeaders, additionalHeaders, accountId, token);
headers.set("OpenAI-Beta", "responses=experimental");
headers.set("accept", "text/event-stream");
headers.set("content-type", "application/json");
if (sessionId) {
headers.set("session_id", sessionId);
headers.set("x-client-request-id", sessionId);
}
return headers;
}
function buildWebSocketHeaders(initHeaders, additionalHeaders, accountId, token, requestId) {
const headers = buildBaseCodexHeaders(initHeaders, additionalHeaders, accountId, token);
headers.delete("accept");
headers.delete("content-type");
headers.delete("OpenAI-Beta");
headers.delete("openai-beta");
headers.set("OpenAI-Beta", OPENAI_BETA_RESPONSES_WEBSOCKETS);
headers.set("x-client-request-id", requestId);
headers.set("session_id", requestId);
return headers;
}
function toLintErrorObject(value, fallbackMessage) {
if (value instanceof Error) return value;
if (typeof value === "string") return new Error(value);
const error = new Error(fallbackMessage, { cause: value });
if (typeof value === "object" && value !== null || typeof value === "function") Object.assign(error, value);
return error;
}
//#endregion
export { closeOpenAICodexWebSocketSessions, extractOpenAICodexAccountId, getOpenAICodexWebSocketDebugStats, resetOpenAICodexWebSocketDebugStats, streamOpenAICodexResponses, streamSimpleOpenAICodexResponses };