openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
246 lines (245 loc) • 9.88 kB
JavaScript
import { l as toErrorObject } from "./error-coercion-D_-xJ90S.js";
import "./realtime-voice-provider-CoGArOTN.js";
import { c as createOpenAIQuicksilverCall, r as buildOpenAIQuicksilverSession } from "./realtime-quicksilver-wire-0W0-YPkD.js";
import { t as OpenAIQuicksilverDelegationController } from "./realtime-quicksilver-delegation-controller-CT_wFUYO.js";
import { n as reserveOpenAIQuicksilverSession, t as releaseOpenAIQuicksilverSession } from "./realtime-quicksilver-session-limit-D_5q6nOg.js";
import { t as connectOpenAIQuicksilverSideband } from "./realtime-quicksilver-sideband-ynkyOe4x.js";
import { n as OpenAIQuicksilverPendingAudio } from "./realtime-quicksilver-audio-buffer-ZY82NjqB.js";
import { randomUUID } from "node:crypto";
import WebSocket$1 from "ws";
//#region extensions/openai/realtime-quicksilver-gateway-bridge.ts
const RELAY_SAMPLE_RATE = 24e3;
const QUICKSILVER_SESSION_TTL_MS = 18e5;
const QUICKSILVER_CONNECT_TIMEOUT_MS = 3e4;
const WEBSOCKET_OPEN = 1;
function normalizeSidebandCloseReason(reason) {
return (typeof reason === "string" ? reason : reason?.toString("utf8") ?? "").replaceAll(/\s+/g, " ").trim().slice(0, 180);
}
function describeSidebandClose(code, reason) {
return `OpenAI GPT-Live sideband closed (code ${code}${reason ? `: ${reason}` : ""})`;
}
function connectAbortError(signal) {
return signal.reason instanceof Error ? signal.reason : new Error("GPT-Live gateway relay startup stopped", { cause: signal.reason });
}
function waitForConnectStep(promise, signal) {
if (signal.aborted) return Promise.reject(connectAbortError(signal));
return new Promise((resolve, reject) => {
const onAbort = () => {
signal.removeEventListener("abort", onAbort);
reject(connectAbortError(signal));
};
signal.addEventListener("abort", onAbort, { once: true });
promise.then((value) => {
signal.removeEventListener("abort", onAbort);
resolve(value);
}, (error) => {
signal.removeEventListener("abort", onAbort);
reject(toErrorObject(error, "OpenAI GPT-Live gateway relay failed"));
});
});
}
/** Realtime voice bridge used only when a Gateway relay injects the agent runner. */
var OpenAIQuicksilverGatewayBridge = class {
constructor(config, runtime) {
this.config = config;
this.runtime = runtime;
this.supportsToolResultContinuation = false;
this.supportsToolResultSuppression = false;
this.abortController = new AbortController();
this.connected = false;
this.closed = false;
this.closeNotified = false;
this.pendingAudio = new OpenAIQuicksilverPendingAudio();
this.ready = false;
if (config.runAgentConsult) this.delegations = new OpenAIQuicksilverDelegationController({
getSocket: () => this.sideband?.socket,
handleDelegationInput: config.handleDelegationInput,
logger: config.logger,
onError: config.onError,
onFatalError: (error) => this.fail(error),
onSessionStarted: (expiresAt) => {
if (expiresAt !== void 0) this.scheduleExpiry(Math.min(QUICKSILVER_SESSION_TTL_MS, Math.max(0, expiresAt * 1e3 - Date.now())));
if (!this.ready) {
this.ready = true;
this.config.onReady?.();
}
},
onTranscript: (role, text, done) => this.config.onTranscript?.(role, text, done),
onWireEventType: (eventType) => {
this.config.onEvent?.({
direction: "server",
type: eventType
});
if (eventType === "output_audio_buffer.cleared") this.config.onClearAudio("barge-in");
},
runAgentConsult: config.runAgentConsult,
signal: this.abortController.signal
}, this.runtime.formatErrorMessage);
}
connect() {
if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("GPT-Live gateway relay bridge is closed"));
this.connectPromise ??= this.connectInternal();
return this.connectPromise;
}
sendAudio(audio) {
if (this.peer) this.peer.sendAudio(audio);
else if (!this.closed && !this.abortController.signal.aborted) this.pendingAudio.append(audio);
}
setMediaTimestamp(_ts) {}
sendUserMessage(text) {
this.delegations?.sendSessionContext(text, "speakable");
}
submitToolResult() {
throw new Error("GPT-Live gateway relay uses provider-owned agent delegations");
}
acknowledgeMark() {}
close(options) {
this.teardown("completed", void 0, options?.disposition ?? "abort");
}
isConnected() {
return this.connected && !this.closed;
}
async connectInternal() {
if (!this.config.runAgentConsult) throw new Error("OpenAI GPT-Live gateway relay requires the Gateway agent-consult runtime");
const audioFormat = this.config.audioFormat;
if (audioFormat && (audioFormat.encoding !== "pcm16" || audioFormat.sampleRateHz !== RELAY_SAMPLE_RATE || audioFormat.channels !== 1)) throw new Error("OpenAI GPT-Live gateway relay requires mono PCM16 audio at 24 kHz");
reserveOpenAIQuicksilverSession(this);
const connectSignal = AbortSignal.any([this.abortController.signal, AbortSignal.timeout(this.config.connectTimeoutMs ?? QUICKSILVER_CONNECT_TIMEOUT_MS)]);
try {
const peerPromise = (this.config.createPeer ?? (async (callbacks, signal) => {
const { OpenAIQuicksilverAudioPeer } = await import("./extensions/openai/realtime-quicksilver-peer.runtime.js");
return await OpenAIQuicksilverAudioPeer.create({
callbacks,
signal
});
}))({
onAudio: (audio) => this.config.onAudio(audio),
onError: (error) => this.fail(error),
onRtpPacket: () => this.config.onEvent?.({
direction: "server",
type: "output_audio.rtp"
})
}, connectSignal);
peerPromise.then((peer) => {
if (connectSignal.aborted || this.closed) peer.close();
}, () => void 0);
this.peer = await waitForConnectStep(peerPromise, connectSignal);
if (this.pendingAudio.length > 0) {
const pendingAudio = this.pendingAudio;
this.pendingAudio = new OpenAIQuicksilverPendingAudio();
this.peer.adoptPendingAudio(pendingAudio);
}
const offerSdp = await waitForConnectStep(this.peer.createOffer(), connectSignal);
const auth = await waitForConnectStep(this.config.resolveAuth(), connectSignal);
const requestIds = {
realtimeSessionId: randomUUID(),
sessionId: randomUUID(),
threadId: randomUUID()
};
const call = await waitForConnectStep(createOpenAIQuicksilverCall({
auth,
requestIds,
sdp: offerSdp,
session: buildOpenAIQuicksilverSession({
model: this.config.model,
hostControlsInput: Boolean(this.config.handleDelegationInput),
instructions: this.config.instructions,
voice: this.config.voice
}),
signal: connectSignal,
fetchImpl: this.config.fetchImpl
}, this.runtime), connectSignal);
if (call.kind !== "gpt-live") throw new Error("GPT-Live gateway relay unexpectedly used the GA realtime call shape");
await waitForConnectStep(this.peer.applyAnswer(call.answerSdp), connectSignal);
const createSocket = this.config.webSocketFactory ?? ((url, options) => new WebSocket$1(url, options));
const connected = await connectOpenAIQuicksilverSideband({
auth,
createSocket,
requestIds,
signal: connectSignal,
url: call.sidebandUrl
}, this.runtime);
if (connectSignal.aborted) {
connected.socket.close(1e3, "session stopped");
throw connectSignal.reason;
}
this.sideband = {
socket: connected.socket,
requestIds
};
this.attachSidebandHandlers(connected.socket);
const terminalEvent = connected.detachBuffer();
this.connected = true;
this.scheduleExpiry(QUICKSILVER_SESSION_TTL_MS);
for (const frame of connected.bufferedFrames) this.handleSidebandFrame(frame.data, frame.isBinary);
if (terminalEvent?.kind === "error") throw terminalEvent.error;
if (terminalEvent?.kind === "close") {
const reason = normalizeSidebandCloseReason(terminalEvent.reason);
throw new Error(describeSidebandClose(terminalEvent.code, reason));
}
} catch (error) {
this.releaseResources("abort");
throw toErrorObject(error, "OpenAI GPT-Live gateway relay failed");
}
}
attachSidebandHandlers(socket) {
socket.on("message", (data, isBinary) => this.handleSidebandFrame(data, isBinary));
socket.on("error", (error) => this.fail(error));
socket.on("close", (code, rawReason) => {
const closeCode = code ?? 1006;
const reason = normalizeSidebandCloseReason(rawReason);
if (!this.closed) {
if (closeCode === 1e3) this.teardown("completed");
else this.fail(new Error(describeSidebandClose(closeCode, reason)));
}
});
}
handleSidebandFrame(data, isBinary) {
this.delegations?.handleFrame(data, isBinary);
}
scheduleExpiry(ttlMs) {
if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(() => this.teardown("completed"), Math.max(0, ttlMs));
this.timer.unref?.();
}
fail(error) {
this.teardown("error", () => this.config.onError?.(error));
}
teardown(reason, beforeClose, disposition = "abort") {
if (this.closed) return;
this.closed = true;
this.releaseResources(disposition);
try {
beforeClose?.();
} finally {
if (!this.closeNotified) {
this.closeNotified = true;
this.config.onClose?.(reason);
}
}
}
releaseResources(disposition) {
releaseOpenAIQuicksilverSession(this);
this.connected = false;
this.pendingAudio.clear();
if (disposition === "detach") this.delegations?.detach();
else this.delegations?.stop(/* @__PURE__ */ new Error("GPT-Live delegation stopped"));
this.abortController.abort(/* @__PURE__ */ new Error("GPT-Live gateway relay bridge closed"));
if (this.timer) {
clearTimeout(this.timer);
this.timer = void 0;
}
const socket = this.sideband?.socket;
this.sideband = void 0;
if (socket?.readyState === WEBSOCKET_OPEN) try {
socket.send(JSON.stringify({ type: "session.close" }));
} catch {}
try {
socket?.close(1e3, "session closed");
} catch {}
this.peer?.close();
this.peer = void 0;
}
};
//#endregion
export { OpenAIQuicksilverGatewayBridge as t };