
























@@ -6,6 +6,8 @@ import {
66} from "openclaw/plugin-sdk/proxy-capture";
77import type {
88RealtimeVoiceBridge,
9+RealtimeVoiceBrowserSession,
10+RealtimeVoiceBrowserSessionCreateRequest,
911RealtimeVoiceBridgeCreateRequest,
1012RealtimeVoiceProviderConfig,
1113RealtimeVoiceProviderPlugin,
@@ -59,6 +61,8 @@ type OpenAIRealtimeVoiceBridgeConfig = RealtimeVoiceBridgeCreateRequest & {
5961azureApiVersion?: string;
6062};
616364+const OPENAI_REALTIME_DEFAULT_MODEL = "gpt-realtime-1.5";
65+6266type RealtimeEvent = {
6367type: string;
6468delta?: string;
@@ -117,7 +121,7 @@ function base64ToBuffer(b64: string): Buffer {
117121}
118122119123class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
120-private static readonly DEFAULT_MODEL = "gpt-realtime-1.5";
124+private static readonly DEFAULT_MODEL = OPENAI_REALTIME_DEFAULT_MODEL;
121125private static readonly MAX_RECONNECT_ATTEMPTS = 5;
122126private static readonly BASE_RECONNECT_DELAY_MS = 1000;
123127private static readonly CONNECT_TIMEOUT_MS = 10_000;
@@ -579,6 +583,77 @@ class OpenAIRealtimeVoiceBridge implements RealtimeVoiceBridge {
579583}
580584}
581585586+function readStringField(value: unknown, key: string): string | undefined {
587+if (!value || typeof value !== "object") {
588+return undefined;
589+}
590+const raw = (value as Record<string, unknown>)[key];
591+return typeof raw === "string" && raw.trim() ? raw.trim() : undefined;
592+}
593+594+async function createOpenAIRealtimeBrowserSession(
595+req: RealtimeVoiceBrowserSessionCreateRequest,
596+): Promise<RealtimeVoiceBrowserSession> {
597+const config = normalizeProviderConfig(req.providerConfig);
598+const apiKey = config.apiKey || process.env.OPENAI_API_KEY;
599+if (!apiKey) {
600+throw new Error("OpenAI API key missing");
601+}
602+if (config.azureEndpoint || config.azureDeployment) {
603+throw new Error("OpenAI Realtime browser sessions do not support Azure endpoints yet");
604+}
605+606+const model = req.model ?? config.model ?? OPENAI_REALTIME_DEFAULT_MODEL;
607+const voice = (req.voice ?? config.voice ?? "alloy") as OpenAIRealtimeVoice;
608+const session: Record<string, unknown> = {
609+type: "realtime",
610+ model,
611+instructions: req.instructions,
612+audio: {
613+output: { voice },
614+},
615+};
616+if (req.tools && req.tools.length > 0) {
617+session.tools = req.tools;
618+session.tool_choice = "auto";
619+}
620+621+const response = await fetch("https://api.openai.com/v1/realtime/client_secrets", {
622+method: "POST",
623+headers: {
624+Authorization: `Bearer ${apiKey}`,
625+"Content-Type": "application/json",
626+},
627+body: JSON.stringify({ session }),
628+});
629+if (!response.ok) {
630+const detail = await response.text().catch(() => "");
631+throw new Error(
632+`OpenAI Realtime browser session failed (${response.status}): ${detail || response.statusText}`,
633+);
634+}
635+const payload = (await response.json()) as unknown;
636+const nestedSecret =
637+payload && typeof payload === "object"
638+ ? (payload as Record<string, unknown>).client_secret
639+ : undefined;
640+const clientSecret = readStringField(payload, "value") ?? readStringField(nestedSecret, "value");
641+if (!clientSecret) {
642+throw new Error("OpenAI Realtime browser session did not return a client secret");
643+}
644+const expiresAt =
645+payload && typeof payload === "object"
646+ ? (payload as Record<string, unknown>).expires_at
647+ : undefined;
648+return {
649+provider: "openai",
650+ clientSecret,
651+ model,
652+ voice,
653+ ...(typeof expiresAt === "number" ? { expiresAt } : {}),
654+};
655+}
656+582657export function buildOpenAIRealtimeVoiceProvider(): RealtimeVoiceProviderPlugin {
583658return {
584659id: "openai",
@@ -607,6 +682,7 @@ export function buildOpenAIRealtimeVoiceProvider(): RealtimeVoiceProviderPlugin
607682azureApiVersion: config.azureApiVersion,
608683});
609684},
685+createBrowserSession: createOpenAIRealtimeBrowserSession,
610686};
611687}
612688此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。