openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
311 lines (310 loc) • 11.7 kB
JavaScript
import { M as realtimeVoiceAudioDurationMs, k as REALTIME_VOICE_AUDIO_FORMAT_G711_ULAW_8KHZ } from "./agent-run-control-shared-zVaKVJuy.js";
import "./realtime-voice-provider-CoGArOTN.js";
import { D as parsePlaybackMarkSequence, g as buildOpenAIRealtimeTurnDetectionConfig, h as buildOpenAIRealtimeGaSessionPolicy, w as normalizeOpenAIRealtimeTools } from "./realtime-voice-session-policy-BbSNlXZ4.js";
import { randomUUID } from "node:crypto";
//#region extensions/openai/realtime-voice-protocol.ts
var OpenAIRealtimeProtocol = class {
static {
this.MAX_TOOL_ARGUMENT_BYTES = 256e3;
}
static {
this.MAX_COMPLETED_TOOL_CALL_IDS = 1024;
}
constructor(config, runtime) {
this.config = config;
this.runtime = runtime;
this.supportsToolResultContinuation = true;
this.supportsToolResultSuppression = true;
this.nextMarkSequence = 1;
this.oldestOutstandingMarkSequence = null;
this.latestOutstandingMarkSequence = null;
this.responseActive = false;
this.responseCreateState = "idle";
this.manualResponseCreateEventId = null;
this.responseCancelInFlight = false;
this.manualResponseCancelEventId = null;
this.responseCreatePending = false;
this.autoRespondSuppressedForManualResponse = false;
this.continuingToolCallIds = /* @__PURE__ */ new Set();
this.pendingToolCallIds = /* @__PURE__ */ new Set();
this.latestMediaTimestamp = 0;
this.outputAudioGeneration = 0;
this.interruptingPlayback = false;
this.assistantAudioItem = null;
this.completedToolCallIds = /* @__PURE__ */ new Set();
this.standaloneSpeechQueue = [];
this.standaloneSpeechActive = false;
this.standaloneSpeechEventId = null;
this.audioFormat = config.audioFormat ?? REALTIME_VOICE_AUDIO_FORMAT_G711_ULAW_8KHZ;
}
setMediaTimestamp(ts) {
this.latestMediaTimestamp = ts;
}
acknowledgeMark(markName) {
const oldest = this.oldestOutstandingMarkSequence;
const latest = this.latestOutstandingMarkSequence;
if (oldest === null || latest === null) return;
const acknowledgedSequence = markName === void 0 ? oldest : parsePlaybackMarkSequence(markName);
if (acknowledgedSequence === void 0 || acknowledgedSequence < oldest || acknowledgedSequence > latest) return;
if (acknowledgedSequence === latest) {
this.oldestOutstandingMarkSequence = null;
this.latestOutstandingMarkSequence = null;
return;
}
this.oldestOutstandingMarkSequence = acknowledgedSequence + 1;
}
sendSessionUpdate() {
if (this.usesAzureDeploymentRealtimeApi()) {
this.sendEvent(this.buildAzureDeploymentSessionUpdate());
return;
}
this.sendEvent(this.buildGaSessionUpdate());
}
buildGaSessionUpdate() {
const cfg = this.config;
return {
type: "session.update",
session: cfg.gaSessionPolicy ?? buildOpenAIRealtimeGaSessionPolicy({
audioFormat: this.audioFormat,
autoRespondToAudio: cfg.autoRespondToAudio,
instructions: cfg.instructions,
interruptResponseOnInputAudio: cfg.interruptResponseOnInputAudio,
language: cfg.language,
model: cfg.model ?? "gpt-realtime-2.1",
noiseReduction: null,
prefixPaddingMs: cfg.prefixPaddingMs,
reasoningEffort: cfg.reasoningEffort,
silenceDurationMs: cfg.silenceDurationMs,
tools: normalizeOpenAIRealtimeTools(cfg.tools, this.runtime.warn),
vadThreshold: cfg.vadThreshold,
voice: cfg.voice ?? "alloy"
})
};
}
usesAzureDeploymentRealtimeApi() {
return Boolean(this.config.azureEndpoint && this.config.azureDeployment);
}
buildAzureDeploymentSessionUpdate() {
const cfg = this.config;
const format = this.resolveLegacyRealtimeAudioFormat();
const tools = normalizeOpenAIRealtimeTools(cfg.tools, this.runtime.warn, 64);
return {
type: "session.update",
session: {
modalities: ["text", "audio"],
instructions: cfg.instructions,
voice: cfg.voice ?? "alloy",
input_audio_format: format,
output_audio_format: format,
input_audio_transcription: {
model: "whisper-1",
...cfg.language ? { language: cfg.language } : {}
},
turn_detection: this.buildTurnDetectionConfig(),
temperature: cfg.temperature ?? .8,
...tools ? {
tools,
tool_choice: "auto"
} : {}
}
};
}
buildTurnDetectionConfig(options) {
return buildOpenAIRealtimeTurnDetectionConfig({
autoRespondToAudio: this.config.autoRespondToAudio,
createResponse: options?.createResponse,
includeInterruptResponse: options?.includeInterruptResponse,
interruptResponseOnInputAudio: this.config.interruptResponseOnInputAudio,
prefixPaddingMs: this.config.prefixPaddingMs,
silenceDurationMs: this.config.silenceDurationMs,
vadThreshold: this.config.vadThreshold
});
}
sendAutoResponseSessionUpdate(createResponse) {
const azureDeployment = this.usesAzureDeploymentRealtimeApi();
const turnDetection = this.buildTurnDetectionConfig({
createResponse,
includeInterruptResponse: !azureDeployment
});
if (azureDeployment) {
this.sendEvent({
type: "session.update",
session: { turn_detection: turnDetection }
});
return;
}
this.sendEvent({
type: "session.update",
session: {
type: "realtime",
audio: { input: { turn_detection: turnDetection } }
}
});
}
resolveLegacyRealtimeAudioFormat() {
return this.audioFormat.encoding === "pcm16" ? "pcm16" : "g711_ulaw";
}
releaseResponseState(options = {}) {
this.responseActive = false;
this.responseCreateState = "idle";
this.manualResponseCreateEventId = null;
this.responseCancelInFlight = false;
this.manualResponseCancelEventId = null;
if (this.standaloneSpeechActive) {
this.standaloneSpeechActive = false;
this.standaloneSpeechEventId = null;
}
if (options.drain !== false) this.drainResponseQueue();
}
drainResponseQueue() {
if (this.interruptingPlayback || this.responseActive || this.responseCreateState !== "idle" || this.responseCancelInFlight) return;
if (this.standaloneSpeechQueue.length > 0) this.flushStandaloneSpeech();
else if (this.responseCreatePending) this.flushPendingResponseCreate();
else this.restoreAutoRespondAfterManualResponse();
}
handleBargeIn(options) {
if (this.interruptingPlayback) return;
this.interruptingPlayback = true;
try {
this.interruptPlayback(options);
} finally {
this.interruptingPlayback = false;
}
this.drainResponseQueue();
}
interruptPlayback(options) {
const assistantAudioItem = this.assistantAudioItem;
const force = options?.force === true;
const shouldInterruptProvider = assistantAudioItem !== null && (this.oldestOutstandingMarkSequence !== null || options?.audioPlaybackActive === true || force);
const playbackItems = (this.config.getPlaybackState ? this.config.getPlaybackState() : shouldInterruptProvider && assistantAudioItem ? [{
itemId: assistantAudioItem.itemId,
audioEndMs: Math.min(Math.floor(realtimeVoiceAudioDurationMs(this.audioFormat, assistantAudioItem.bytes)), Math.max(0, this.latestMediaTimestamp - assistantAudioItem.startTimestamp))
}] : []).map(({ itemId, audioEndMs }) => ({
itemId,
audioEndMs
}));
const audioEndMs = playbackItems.reduce((played, item) => played + item.audioEndMs, 0);
const minBargeInAudioEndMs = this.config.minBargeInAudioEndMs ?? 250;
if (!force && playbackItems.length > 0 && audioEndMs < minBargeInAudioEndMs) {
this.config.onEvent?.({
direction: "client",
type: "conversation.item.truncate.skipped",
detail: `reason=barge-in audioEndMs=${audioEndMs} minAudioEndMs=${minBargeInAudioEndMs}`
});
return;
}
if (this.responseCreateState === "preparing") this.releaseResponseState({ drain: false });
const cancelResponse = (options?.audioPlaybackActive === true || force) && (this.responseActive || this.responseCreateState === "in-flight") && !this.responseCancelInFlight;
this.outputAudioGeneration += 1;
this.assistantAudioItem = null;
this.clearOutstandingMarks();
if (this.responseActive || this.responseCreateState === "in-flight") this.responseCancelInFlight = true;
if (cancelResponse) {
const eventId = `openclaw-response-cancel-${randomUUID()}`;
this.manualResponseCancelEventId = eventId;
this.sendEvent({
type: "response.cancel",
event_id: eventId
}, "reason=barge-in");
}
for (const item of playbackItems) this.sendEvent({
type: "conversation.item.truncate",
item_id: item.itemId,
content_index: 0,
audio_end_ms: item.audioEndMs
}, `reason=barge-in audioEndMs=${item.audioEndMs}`);
this.config.onClearAudio("barge-in");
}
requestResponseCreate(options) {
if (this.interruptingPlayback || this.responseActive || this.responseCreateState !== "idle" || this.responseCancelInFlight || this.continuingToolCallIds.size > 0 || this.pendingToolCallIds.size > 0) {
this.responseCreatePending = true;
return;
}
this.responseCreatePending = false;
this.responseCreateState = "preparing";
this.suppressAutoRespondForManualResponse();
if (this.responseCreateState !== "preparing") return;
this.responseCreateState = "in-flight";
const eventId = `openclaw-response-create-${randomUUID()}`;
this.manualResponseCreateEventId = eventId;
this.sendEvent({
type: "response.create",
event_id: eventId,
...options?.toolChoice ? { response: {
output_modalities: ["audio"],
tool_choice: options.toolChoice
} } : {}
});
}
flushStandaloneSpeech() {
if (this.interruptingPlayback || this.standaloneSpeechActive || this.responseActive || this.responseCreateState !== "idle" || this.responseCancelInFlight) return;
const text = this.standaloneSpeechQueue.shift();
if (!text) return;
const eventId = `openclaw-standalone-speech-${randomUUID()}`;
this.standaloneSpeechActive = true;
this.standaloneSpeechEventId = eventId;
this.responseCreateState = "in-flight";
this.sendEvent({
type: "response.create",
event_id: eventId,
response: {
conversation: "none",
output_modalities: ["audio"],
input: [{
type: "message",
role: "user",
content: [{
type: "input_text",
text
}]
}]
}
});
}
suppressAutoRespondForManualResponse() {
if (this.config.autoRespondToAudio === false || this.autoRespondSuppressedForManualResponse) return;
this.autoRespondSuppressedForManualResponse = true;
this.sendAutoResponseSessionUpdate(false);
}
restoreAutoRespondAfterManualResponse() {
if (!this.autoRespondSuppressedForManualResponse) return;
this.autoRespondSuppressedForManualResponse = false;
this.sendAutoResponseSessionUpdate(true);
}
flushPendingResponseCreate() {
if (!this.responseCreatePending) return;
this.responseCreatePending = false;
this.requestResponseCreate();
}
resetRealtimeSessionState() {
this.outputAudioGeneration += 1;
this.clearOutstandingMarks();
this.assistantAudioItem = null;
this.responseActive = false;
this.responseCreateState = "idle";
this.manualResponseCreateEventId = null;
this.responseCancelInFlight = false;
this.manualResponseCancelEventId = null;
this.responseCreatePending = false;
this.autoRespondSuppressedForManualResponse = false;
this.continuingToolCallIds.clear();
this.pendingToolCallIds.clear();
this.completedToolCallIds.clear();
this.standaloneSpeechQueue = [];
this.standaloneSpeechActive = false;
this.standaloneSpeechEventId = null;
}
createPlaybackMark() {
const sequence = this.nextMarkSequence;
this.nextMarkSequence += 1;
if (this.oldestOutstandingMarkSequence === null) this.oldestOutstandingMarkSequence = sequence;
this.latestOutstandingMarkSequence = sequence;
return `audio-${sequence}`;
}
clearOutstandingMarks() {
this.oldestOutstandingMarkSequence = null;
this.latestOutstandingMarkSequence = null;
}
};
//#endregion
export { OpenAIRealtimeProtocol as t };