openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
341 lines (340 loc) • 15.3 kB
JavaScript
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.mjs";
import { n as readResponseTextPrefix } from "./http-response-body-L4SAGcXJ.mjs";
import "./response-limit-runtime-EJhmMies.mjs";
import "./realtime-voice-provider-D_Fp61Jy.mjs";
import { i as isOpenAIGptLiveModel, r as isOpenAIGptLiveApiModel, s as resolveOpenAIQuicksilverVoice } from "./realtime-quicksilver-0VQEPDSP.mjs";
import { n as createOpenAILiveCall, t as OPENAI_LIVE_SESSIONS_URL } from "./realtime-live-api-C4qRTnZ1.mjs";
import { n as buildOpenAIQuicksilverBackgroundContext, t as OPENAI_QUICKSILVER_HOST_CONTROL_INSTRUCTIONS } from "./realtime-quicksilver-instructions-CkYwSLJz.mjs";
import "./realtime-quicksilver-events-BptwDZ_Q.mjs";
import { randomBytes, randomUUID } from "node:crypto";
//#region extensions/openai/realtime-quicksilver-wire.ts
const OPENAI_QUICKSILVER_APPEND_MAX_BYTES = 500;
const OPENAI_QUICKSILVER_DELEGATION_RESULT_MAX_CHARS = 1800;
const OPENAI_QUICKSILVER_CONTEXT_MAX_ENTRIES = 16;
const OPENAI_QUICKSILVER_CONTEXT_MAX_ITEM_CHARS = 800;
const OPENAI_QUICKSILVER_CONTEXT_MAX_UTF8_BYTES = 8e3;
const OPENAI_QUICKSILVER_CALL_URL = "https://api.openai.com/v1/live";
const OPENAI_CHATGPT_QUICKSILVER_CALL_URL = "https://chatgpt.com/backend-api/codex/realtime/calls?intent=quicksilver&architecture=avas";
const OPENAI_REALTIME_CALL_URL = "https://api.openai.com/v1/realtime/calls";
const OPENAI_REALTIME_ERROR_BODY_MAX_BYTES = 16384;
const OPENAI_REALTIME_ERROR_DETAIL_MAX_CHARS = 500;
const OPENAI_REALTIME_SDP_ANSWER_MAX_BYTES = 262144;
const OPENAI_REALTIME_LOCATION_MAX_BYTES = 512;
const OPENAI_REALTIME_CALL_ID_RE = /^[A-Za-z0-9_-]{1,128}$/u;
function redactOpenAIRealtimeErrorDetail(text, auth, model, redactSensitiveText) {
let redacted = text;
const exactSecrets = [
auth.token,
auth.type === "oauth" ? auth.accountId : void 0,
model
];
for (const secret of exactSecrets) if (secret) redacted = redacted.split(secret).join("[REDACTED]");
return redactSensitiveText(redacted, { mode: "tools" });
}
function createOpenAIQuicksilverRequestIds() {
return {
realtimeSessionId: randomUUID(),
sessionId: randomUUID(),
threadId: randomUUID()
};
}
var OpenAIQuicksilverCallError = class extends Error {
constructor(message, status) {
super(message);
this.status = status;
this.name = "OpenAIQuicksilverCallError";
}
};
function buildOpenAIQuicksilverSession(params) {
const history = boundOpenAIQuicksilverContextItems(params.initialItems ?? []);
const publicApi = isOpenAIGptLiveApiModel(params.model);
const instructions = [params.instructions?.trim(), params.hostControlsInput ? OPENAI_QUICKSILVER_HOST_CONTROL_INSTRUCTIONS : void 0].filter(Boolean).join("\n\n");
const initialItems = (params.hostControlsInput ? [] : history).map((item) => ({
type: "message",
role: item.role,
content: [{
type: item.role === "assistant" ? "output_text" : "input_text",
text: item.text
}]
}));
return {
model: params.model,
instructions: instructions + (params.hostControlsInput ? buildOpenAIQuicksilverBackgroundContext(history, OPENAI_QUICKSILVER_CONTEXT_MAX_UTF8_BYTES) : ""),
audio: { output: { voice: resolveOpenAIQuicksilverVoice(params.model, params.voice) } },
delegation: params.hostControlsInput && !publicApi ? {
type: "client",
ack_filler: false
} : { type: "client" },
...initialItems.length > 0 ? publicApi ? { input: initialItems } : { initial_items: initialItems } : {}
};
}
/** Builds the initial WebSocket frame for the selected Live protocol. */
function buildOpenAIQuicksilverSessionUpdate(params) {
const configured = buildOpenAIQuicksilverSession(params);
if (isOpenAIGptLiveApiModel(params.model)) return {
type: "session.start",
session: configured
};
const { model: _model, ...session } = configured;
return {
type: "session.update",
session
};
}
function buildOpenAIQuicksilverWebSocketUrl(model) {
if (isOpenAIGptLiveApiModel(model)) return OPENAI_LIVE_SESSIONS_URL.replace("https:", "wss:");
const url = new URL(OPENAI_QUICKSILVER_CALL_URL);
url.protocol = "wss:";
url.searchParams.set("model", model);
return url.toString();
}
function truncateOpenAIQuicksilverContextText(text, maxBytes) {
let result = "";
let bytes = 0;
let characters = 0;
for (const character of text) {
const characterBytes = Buffer.byteLength(character, "utf8");
if (characters >= OPENAI_QUICKSILVER_CONTEXT_MAX_ITEM_CHARS || bytes + characterBytes > maxBytes) break;
result += character;
bytes += characterBytes;
characters += 1;
}
return result;
}
function boundOpenAIQuicksilverContextItems(items) {
let remainingBytes = OPENAI_QUICKSILVER_CONTEXT_MAX_UTF8_BYTES;
const newestFirst = [];
for (let index = items.length - 1; index >= 0 && newestFirst.length < OPENAI_QUICKSILVER_CONTEXT_MAX_ENTRIES; index -= 1) {
const item = items[index];
if (!item || remainingBytes <= 0) continue;
const text = truncateOpenAIQuicksilverContextText(item.text, remainingBytes);
if (!text) continue;
newestFirst.push({
role: item.role,
text
});
remainingBytes -= Buffer.byteLength(text, "utf8");
}
return newestFirst.toReversed();
}
function openAIQuicksilverAuthHeaders(auth, requestIds, runtime, url = OPENAI_QUICKSILVER_CALL_URL) {
const baseUrl = url.replace(/^wss:/u, "https:");
return openAIRealtimeAuthHeaders({
auth,
requestIds,
baseUrl,
includeQuicksilverAlpha: !baseUrl.startsWith(OPENAI_LIVE_SESSIONS_URL)
}, runtime);
}
function openAIRealtimeAuthHeaders(params, { resolveProviderRequestHeaders }) {
return {
...resolveProviderRequestHeaders({
provider: "openai",
baseUrl: params.baseUrl,
capability: "audio",
transport: "http",
defaultHeaders: {}
}) ?? {},
Authorization: `Bearer ${params.auth.token}`,
...params.includeQuicksilverAlpha ? { "OpenAI-Alpha": "quicksilver=v2" } : {},
"session-id": params.requestIds.sessionId,
"thread-id": params.requestIds.threadId,
"x-session-id": params.requestIds.realtimeSessionId,
...params.auth.type === "oauth" ? { "chatgpt-account-id": params.auth.accountId } : {}
};
}
function buildOpenAIQuicksilverMultipartBody(params) {
const sessionJson = JSON.stringify(params.session);
let boundary;
do
boundary = `openclaw-quicksilver-${randomBytes(18).toString("hex")}`;
while (params.sdp.includes(boundary) || sessionJson.includes(boundary));
return {
body: [
`--${boundary}\r\n`,
"Content-Disposition: form-data; name=\"sdp\"\r\n",
"Content-Type: application/sdp\r\n\r\n",
params.sdp,
"\r\n",
`--${boundary}\r\n`,
"Content-Disposition: form-data; name=\"session\"\r\n",
"Content-Type: application/json\r\n\r\n",
sessionJson,
"\r\n",
`--${boundary}--\r\n`
].join(""),
contentType: `multipart/form-data; boundary=${boundary}`
};
}
function parseOpenAIRealtimeCallLocation(location) {
if (!location) throw new Error("OpenAI Realtime call response is missing the Location header");
if (Buffer.byteLength(location, "utf8") > OPENAI_REALTIME_LOCATION_MAX_BYTES) throw new Error("OpenAI Realtime call response Location header is too large");
let url;
try {
url = new URL(location, OPENAI_REALTIME_CALL_URL);
} catch {
throw new Error("OpenAI Realtime call response Location header is invalid");
}
if (url.origin !== "https://api.openai.com" || url.search || url.hash) throw new Error("OpenAI Realtime call response Location header has an unexpected target");
const match = /^\/v1\/realtime\/calls\/([^/]+)\/?$/u.exec(url.pathname);
if (!match?.[1] || !OPENAI_REALTIME_CALL_ID_RE.test(match[1])) throw new Error("OpenAI Realtime call response Location header has no valid call id");
return match[1];
}
function buildOpenAIRealtimeSidebandUrl(callId) {
if (!OPENAI_REALTIME_CALL_ID_RE.test(callId)) throw new Error("OpenAI Realtime call id is invalid");
const url = new URL("wss://api.openai.com/v1/realtime");
url.searchParams.set("call_id", callId);
return url.toString();
}
function isOpenAIQuicksilverCallId(value) {
return /^rtc_[\w-]+$/.test(value) || /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
}
function decodeOpenAIQuicksilverCallId(params) {
const sessionId = params.openAiSessionId?.trim() ?? "";
if (!params.location) {
if (isOpenAIQuicksilverCallId(sessionId)) return sessionId;
throw new OpenAIQuicksilverCallError(sessionId ? "GPT-Live call response returned an invalid openai-session-id" : "GPT-Live call response missing Location and openai-session-id headers");
}
let pathname;
try {
pathname = new URL(params.location, params.callUrl).pathname;
} catch {
if (isOpenAIQuicksilverCallId(sessionId)) return sessionId;
throw new OpenAIQuicksilverCallError("GPT-Live call response returned an invalid Location");
}
const callId = pathname.split("/").filter(Boolean).find(isOpenAIQuicksilverCallId);
if (!callId) {
if (isOpenAIQuicksilverCallId(sessionId)) return sessionId;
throw new OpenAIQuicksilverCallError("GPT-Live call response Location has no valid call id");
}
return callId;
}
function describeOpenAIQuicksilverCallError(status, detail, auth) {
const normalized = detail.toLowerCase();
if (status === 403) return "GPT-Live rejected the session (403). Verify the selected OpenAI account, model, and GPT-Live voice; this response alone does not identify which was denied.";
if (status === 400 && auth.type === "api-key" && (normalized.includes("model_not_found") || normalized.includes("does not exist or you do not have access"))) return "OpenAI Platform API-key access is unavailable for the selected GPT-Live model. Verify the selected model and Platform account access.";
if (status === 400 && normalized.includes("session.model") && normalized.includes("not allowed")) return "The GPT-Live model value is not permitted. Choose a supported GPT-Live model in Settings > Talk.";
return `GPT-Live call creation failed (${status})`;
}
async function createOpenAIQuicksilverCall(params, runtime) {
const isGptLive = isOpenAIGptLiveModel(params.session.model);
if (params.gaSideband && (isGptLive || params.auth.type !== "api-key")) throw new Error("OpenAI Realtime Gateway control requires a GA model and Platform API key");
if (isOpenAIGptLiveApiModel(params.session.model)) {
if (params.auth.type !== "api-key") throw new Error("GPT-Live API sessions require a Platform API key");
return createOpenAILiveCall({
apiKey: params.auth.token,
sdp: params.sdp,
session: params.session,
onCallAllocated: params.onCallAllocated,
signal: params.signal,
fetchImpl: params.fetchImpl
}, runtime);
}
const chatGptCall = isGptLive && params.auth.type === "oauth";
const callUrl = chatGptCall ? OPENAI_CHATGPT_QUICKSILVER_CALL_URL : isGptLive ? OPENAI_QUICKSILVER_CALL_URL : OPENAI_REALTIME_CALL_URL;
const authHeaders = openAIRealtimeAuthHeaders({
auth: params.auth,
requestIds: params.requestIds,
baseUrl: callUrl,
includeQuicksilverAlpha: isGptLive
}, runtime);
const payload = {
sdp: params.sdp,
session: params.session
};
const requestBody = chatGptCall ? {
body: JSON.stringify(payload),
contentType: "application/json"
} : buildOpenAIQuicksilverMultipartBody(payload);
const response = await (params.fetchImpl ?? fetch)(callUrl, {
method: "POST",
headers: {
...authHeaders,
"Content-Type": requestBody.contentType
},
body: requestBody.body,
signal: params.signal
});
if (!response.ok) {
const providerDetail = await readResponseTextPrefix(response, OPENAI_REALTIME_ERROR_BODY_MAX_BYTES).catch(() => void 0);
const detail = providerDetail?.truncated ? "" : truncateUtf16Safe(redactOpenAIRealtimeErrorDetail(providerDetail?.text.trim() ?? "", params.auth, params.session.model, runtime.redactSensitiveText), OPENAI_REALTIME_ERROR_DETAIL_MAX_CHARS);
throw new OpenAIQuicksilverCallError(isGptLive ? describeOpenAIQuicksilverCallError(response.status, detail, params.auth) : `OpenAI Realtime call creation failed (${response.status})${detail ? `: ${detail}` : ""}`, response.status);
}
let gaCallId;
if (params.gaSideband) try {
gaCallId = parseOpenAIRealtimeCallLocation(response.headers.get("Location"));
params.onCallAllocated(gaCallId);
} catch (error) {
await response.body?.cancel().catch(() => void 0);
throw error;
}
const answerSdp = await runtime.readProviderTextResponse(response, `${isGptLive ? "GPT-Live" : "OpenAI Realtime"} SDP answer`, { maxBytes: OPENAI_REALTIME_SDP_ANSWER_MAX_BYTES });
if (!answerSdp.trim()) throw new OpenAIQuicksilverCallError(`${isGptLive ? "GPT-Live" : "OpenAI Realtime"} call creation returned an empty SDP answer`, response.status);
if (gaCallId) return {
kind: "ga-sideband",
status: response.status,
answerSdp,
callId: gaCallId,
sidebandUrl: buildOpenAIRealtimeSidebandUrl(gaCallId)
};
if (!isGptLive) return {
kind: "ga-realtime",
status: response.status,
answerSdp
};
const callId = decodeOpenAIQuicksilverCallId({
location: response.headers.get("Location"),
openAiSessionId: response.headers.get("openai-session-id"),
callUrl
});
return {
kind: "gpt-live",
status: response.status,
answerSdp,
callId,
sidebandUrl: `wss://api.openai.com/v1/live/${callId}`
};
}
async function hangupOpenAIRealtimeCall(params, { resolveProviderRequestHeaders }) {
if (!OPENAI_REALTIME_CALL_ID_RE.test(params.callId)) throw new Error("OpenAI Realtime call id is invalid");
const url = `${OPENAI_REALTIME_CALL_URL}/${encodeURIComponent(params.callId)}/hangup`;
const headers = resolveProviderRequestHeaders({
provider: "openai",
baseUrl: url,
capability: "audio",
transport: "http",
defaultHeaders: { Authorization: `Bearer ${params.apiKey}` }
}) ?? { Authorization: `Bearer ${params.apiKey}` };
const response = await (params.fetchImpl ?? fetch)(url, {
method: "POST",
headers,
signal: params.signal
});
await response.body?.cancel().catch(() => void 0);
if (!response.ok && response.status !== 404) throw new Error(`OpenAI Realtime call hangup failed (${response.status})`);
}
function chunkOpenAIQuicksilverAppendText(text, maxBytes = OPENAI_QUICKSILVER_APPEND_MAX_BYTES) {
if (Buffer.byteLength(text, "utf8") <= maxBytes) return [text];
const chunks = [];
let current = "";
let currentBytes = 0;
for (const character of text) {
const characterBytes = Buffer.byteLength(character, "utf8");
if (current && currentBytes + characterBytes > maxBytes) {
chunks.push(current);
current = "";
currentBytes = 0;
}
current += character;
currentBytes += characterBytes;
}
if (current) chunks.push(current);
return chunks;
}
/** Bound completed delegation output while preserving under-limit text byte-for-byte. */
function boundOpenAIQuicksilverDelegationResult(text) {
if (text.length <= OPENAI_QUICKSILVER_DELEGATION_RESULT_MAX_CHARS) return text;
return `${truncateUtf16Safe(text, 1784).trimEnd()} [truncated]`;
}
//#endregion
export { buildOpenAIQuicksilverSessionUpdate as a, chunkOpenAIQuicksilverAppendText as c, hangupOpenAIRealtimeCall as d, openAIQuicksilverAuthHeaders as f, buildOpenAIQuicksilverSession as i, createOpenAIQuicksilverCall as l, boundOpenAIQuicksilverContextItems as n, buildOpenAIQuicksilverWebSocketUrl as o, boundOpenAIQuicksilverDelegationResult as r, buildOpenAIRealtimeSidebandUrl as s, OPENAI_QUICKSILVER_CONTEXT_MAX_UTF8_BYTES as t, createOpenAIQuicksilverRequestIds as u };