OpenAI WebSocket 连接

2026-11-06 12:40 更新

Go使用1.26或更高版本,保存main.go,初始化live-demo模块并安装github.com/openai/openai-go/v3@v3.85.1;Java使用17或更高版本、com.openai:openai-java:4.95.0和LiveConnectionWebsocketExample;Ruby使用3.3或更高版本,安装openai 0.110.0与async-websocket并保存为client.rb。接入音源与播放器后,可运行node client.mjs、python client.py、go run .、ruby client.rb或Java程序。 选择所用 API,查看对应连接步骤和事件。

将服务器连接到 GPT-Live

GPT-Live 音频连接推荐 WebRTC,服务端到服务端集成也包括在内。

同时支持 WebSocket。主 WebSocket 双向传音频和 JSON 事件,项目 API 密钥保存在可信服务器。

本指南介绍向 GPT-Live 流式发送音频。监控或控制现有会话见服务端控制;将推理和工具后端接到 Responses API 见 Responses WebSocket 模式。

认证并启动会话

  1. 连接不带查询参数的 wss://api.openai.com/v1/live/sessions。使用 Authorization: Bearer $OPENAI_API_KEY 和示例连接头认证。
  2. 第一条消息发送 session.start,把模型、对话指令、音频格式、声音和委派配置放在 session 中。
  3. 等 session.started 后再发送音频或命令;它包含解析后的会话配置和 ID。

示例使用 Marin、24 kHz PCM16,以及带网页搜索的 Responses 后端。保持对话指令简短。后端指令、工具与权限见委派与工具。

使用 SDK 流式发送音频

Node.js 执行 npm install openai ws,保存为 client.mjs。macOS 或 Linux 的 Python 安装 openai[realtime],保存为 client.py。在服务器设置 OPENAI_API_KEY,使用支持 Live 的 SDK 版本。示例从标准输入读取 24 kHz 单声道原始 PCM16,将相同格式的返回音频写入标准输出,需接入应用采集与播放。日志和转录走标准错误,避免污染音频流。

来源示例 1(javascript):

程序文字译释241:保持简洁,需要最新信息的请求委派给能搜索网页的后端。

程序文字译释247:最终会话用量。

程序文字译释257:会话已就绪。

程序文字译释258:收尾未完成:没有收到session.closed。

程序文字译释259:连接关闭,未取得最终用量。

// [中文] 示例 1:使用 SDK 流式发送音频;以下保留本语言原始示例。
// [中文] 原代码明确设置的模型值:gpt-live-1、gpt-5.6-luna;各值的作用按其所在配置对象区分。
// [中文] 本段构造的事件类型:session.start、session.input_audio.append、session.close;不要将构造载荷等同于收到成功确认。
// [中文] 沿原示例执行这些方法:ws.socket.on、ws.send、ws.on、ws.close、ws.socket.platformSocket.terminate;异步调用及事件接收顺序保留。
// [中文] 将 Base64 载荷解码成原始字节;字节后续写文件、缓冲或播放的位置按本例处理。
// [中文] 需要发送的原始字节先编码为 Base64;编码不会替代重采样或改变编解码格式。
// [中文] 区分发出 session.close 与收到 session.closed;最终用量以接收事件的处理分支为准。
// [中文] 标准输入是原始 PCM,标准输出是返回音频,日志与转录写标准错误,避免破坏音频流。
// [中文] PCM 保持完整双字节采样,输入结束不会自动结束会话;示例通过 SIGINT 请求正常关闭。
import OpenAI from "openai";
import { LiveWS } from "openai/resources/live/ws";

// stdin and stdout carry raw mono PCM16 audio at 24 kHz, not WAV files.
// Supply microphone bytes continuously and play stdout in the same format.
process.stdin.pause();
const ws = new LiveWS(new OpenAI());
let started = false;
let closing = false;
let finalized = false;
let pendingByte = Buffer.alloc(0);

let closeTimeout;

ws.socket.on("open", () => {
  ws.send({
    type: "session.start",
    event_id: "event_start",
    session: {
      model: "gpt-live-1",
      instructions:
        "Be concise. Delegate requests needing current information to the backend, which can search the web.",
      audio: {
        format: { type: "audio/pcm", rate: 24000 },
        output: { voice: "marin" },
      },
      delegation: {
        type: "responses",
        responses: {
          model: "gpt-5.6-luna",
          tools: [{ type: "web_search" }],
          tool_choice: "auto",
        },
      },
    },
  });
});

process.stdin.on("data", (chunk) => {
  if (!started || closing || ws.socket.readyState !== 1) return;
  const bytes = Buffer.concat([pendingByte, chunk]);
  const completeLength = bytes.length - (bytes.length % 2);
  pendingByte = bytes.subarray(completeLength);
  if (completeLength) {
    ws.send({
      type: "session.input_audio.append",
      audio: bytes.subarray(0, completeLength).toString("base64"),
    });
  }
});

// Register the final-event handler before any close command can be sent.
ws.on("event", (event) => {
  if (event.type === "session.started") {
    started = true;
    console.error("Session ready", event.session.id);
    process.stdin.resume();
  } else if (event.type === "session.output_audio.delta") {
    process.stdout.write(Buffer.from(event.delta, "base64"));
  } else if (event.type === "session.closed") {
    finalized = true;
    clearTimeout(closeTimeout);
    process.stdin.pause();
    console.error("Final session usage", event.usage);
    ws.close();
  } else {
    // Includes transcript deltas and nested response.event usage.
    console.error(JSON.stringify(event));
  }
});

process.on("SIGINT", () => {
  if (closing) return;
  if (!started || ws.socket.readyState !== 1) {
    ws.socket.platformSocket.terminate();
    return;
  }
  closing = true;
  process.stdin.pause();
  ws.send({ type: "session.close" });
  closeTimeout = setTimeout(() => {
    console.error("Incomplete finalization: session.closed was not received");
    process.exitCode = 1;
    ws.socket.platformSocket.terminate();
  }, 15_000);
});
ws.on("error", (error) => {
  console.error(error.message);
  process.exitCode = 1;
});
ws.socket.on("close", () => {
  clearTimeout(closeTimeout);
  process.stdin.pause();
  if (!finalized) {
    console.error("Connection closed without final session usage");
    process.exitCode = 1;
  }
});

来源示例 2(python):

沿用程序文字译释241。

沿用程序文字译释247。

沿用程序文字译释257。

沿用程序文字译释259。

程序文字译释260:收尾未完成:缺少session.closed事件。

## [中文] 把标准输入的24 kHz单声道PCM16音频持续送入Live连接,把返回音频写到标准输出;日志与转录走标准错误,关闭时等待会话结束。
import asyncio
import base64
import os
import signal
import sys

from openai import AsyncOpenAI
from openai.types.live.session_config_param import SessionConfigParam


async def main() -> None:
    # stdin/stdout carry raw mono PCM16 at 24 kHz, not WAV files.
    session: SessionConfigParam = {
        "model": "gpt-live-1",
        "instructions": "Be concise. Delegate requests needing current information to the backend, which can search the web.",
        "audio": {
            "format": {"type": "audio/pcm", "rate": 24000},
            "output": {"voice": "marin"},
        },
        "delegation": {
            "type": "responses",
            "responses": {
                "model": "gpt-5.6-luna",
                "tools": [{"type": "web_search"}],
                "tool_choice": "auto",
            },
        },
    }
    loop = asyncio.get_running_loop()
    chunks: asyncio.Queue[bytes] = asyncio.Queue()
    finalized = asyncio.Event()
    closing = False
    pending = b""
    close_task: asyncio.Task[None] | None = None

    def read_audio() -> None:
        chunk = os.read(sys.stdin.fileno(), 4800)
        if chunk:
            chunks.put_nowait(chunk)
        else:
            # EOF does not end a Live session. Use SIGINT to finalize it.
            loop.remove_reader(sys.stdin.fileno())

    async with AsyncOpenAI() as client:
        async with client.live.connect() as connection:

            async def send_audio() -> None:
                nonlocal pending
                while True:
                    chunk = pending + await chunks.get()
                    complete = len(chunk) - len(chunk) % 2
                    pending = chunk[complete:]
                    if complete and not closing:
                        await connection.session.input_audio.append(
                            audio=base64.b64encode(chunk[:complete]).decode("ascii")
                        )

            async def close_session() -> None:
                nonlocal closing
                closing = True
                loop.remove_reader(sys.stdin.fileno())
                await connection.session.close()
                try:
                    await asyncio.wait_for(finalized.wait(), timeout=15)
                except asyncio.TimeoutError:
                    print(
                        "Incomplete finalization: no session.closed event",
                        file=sys.stderr,
                    )
                    await connection.close()

            def request_close() -> None:
                nonlocal close_task
                if close_task is None:
                    close_task = asyncio.create_task(close_session())

            # Start receiving before a close command can be requested.
            await connection.session.start(session=session, event_id="event_start")
            sender = asyncio.create_task(send_audio())
            try:
                async for event in connection:
                    if event.type == "session.started":
                        print("Session ready", event.session.id, file=sys.stderr)
                        loop.add_reader(sys.stdin.fileno(), read_audio)
                        loop.add_signal_handler(signal.SIGINT, request_close)
                    elif event.type == "session.output_audio.delta":
                        sys.stdout.buffer.write(base64.b64decode(event.delta))
                        sys.stdout.buffer.flush()
                    elif event.type == "session.closed":
                        print("Final session usage", event.usage, file=sys.stderr)
                        finalized.set()
                        break
                    else:
                        print(event.model_dump_json(), file=sys.stderr)
            finally:
                loop.remove_reader(sys.stdin.fileno())
                loop.remove_signal_handler(signal.SIGINT)
                sender.cancel()
                await asyncio.gather(sender, return_exceptions=True)
                if close_task is not None:
                    await close_task
            if not finalized.is_set():
                raise RuntimeError("Connection closed without final session usage")


if __name__ == "__main__":
    asyncio.run(main())

连接音源与播放器后运行 node client.mjs 或 python client.py。出现 Session ready 后,以录音采样率持续提供麦克风流。一次管入整份文件不能模拟实时麦克风。音源 EOF 不会结束对话,向进程发送 SIGINT 才请求正常关闭。

示例只连接音频流;采集、缓冲、播放和必要重采样由应用负责。评估模型前,先用实际设备和网络测试这些部分。

选择音频格式

启动时设置 session.audio.format,输入输出共用且会话内不可修改:

  • {"type":"audio/pcm","rate":24000}:24 kHz、单声道、有符号 16 位小端 PCM,默认格式。
  • {"type":"audio/pcm","rate":16000}:16 kHz,同样单声道、有符号 16 位小端 PCM。
  • {"type":"audio/pcmu","rate":8000}:8 kHz G.711 μ-law,每采样一个字节。
  • {"type":"audio/pcma","rate":8000}:8 kHz G.711 A-law,每采样一个字节。

原始字节以 Base64 编码,不含 WAV 等容器头。PCM 块必须包含完整 16 位采样,字节数须为偶数。示例把尾部单字节留到下一输入块。除此之外块边界可任意,但流必须连续有序。

实际采样率不同于配置时应重采样,仅修改设置不会转换字节。G.711 应直接转发编解码字节,移除 PCM 两字节对齐逻辑,并让播放器使用同一编解码器。匹配的 G.711 流可不转 PCM 直通。电话接入见电话集成。

收发事件

每个事件作为 JSON 文本消息发送,音频以 Base64 嵌入:

  • 发送音频:session.input_audio.append 的 audio 为 Base64 原始字节,不返回追加确认。
  • 接收音频:解码 session.output_audio.delta 的 delta,按配置格式有序入播放队列。
  • 接收转录:把 session.input_transcript.delta 和 session.output_transcript.delta 的 delta 追加到对应一方。
  • 接收后端事件:Responses 委派处理各 response.event 封装的内部 event。
  • 处理错误:从 error 读取拒绝命令和会话错误;有 error.client_event_id 时用于匹配命令。

播放由应用音频队列追踪。GPT-Live 主 WebSocket 输出音频不带时间字段,也没有输出音频完成事件。字幕用转录时间戳组织,任务由后端事件追踪。

GPT-Live 随连续音频自行决定何时聆听和说话。response.create 用于启动或续接被委派后端工作。见委派与工具。

配置运行中的会话

Live 模型、初始对话指令、音频格式、声音与委派模式在启动时固定。session.update 仅更新当前模式支持的设置,省略项保留原值,成功返回含完整解析配置的 session.updated。

用 session.instructions.append 增补指令,用 session.input_audio.mute、session.input_audio.unmute 控制输入。输入静音不取消后端工作,也不停止生成语音。上下文、转录、输入控制和用量见管理会话。

关闭会话

结束时发送 session.close,先注册 session.closed 监听,持续接收直到该事件再释放连接。示例最多等 15 秒,未收到终结事件则报告收尾未完成。

保留最终语音用量和已收到后端用量。时长更新是累计快照,不要相加。最终事件前传输失败或超时,最终用量未确认。完整生命周期见管理会话。

Realtime API:WebSockets 广泛支持实时数据传输,适合服务端到服务端连接 Realtime。浏览器与移动端推荐 WebRTC。

后端直接经 WebSocket 连接 API,可用标准 API 密钥,因为令牌只在安全后端可用。

直接连接 Realtime API

通过 WebSocket 连接

以下给出多种连接示例。除 WebSocket URL,还需带 OpenAI 密钥的认证头。应用分配安全标识符时,在 OpenAI-Safety-Identifier 发送稳定、保护隐私的用户标识。

浏览器可用 WebRTC 指南中的临时令牌建立 WebSocket,但多数浏览器或移动场景用 WebRTC 更稳健。

Node.js 的 ws 模块:

来源示例 3(javascript):

程序文字译释224:已连接服务器。

// [中文] 把标准输入的24 kHz单声道PCM16音频持续送入Live连接,把返回音频写到标准输出;日志与转录走标准错误,关闭时等待会话结束。
package main

import (
    "context"
    "encoding/base64"
    "fmt"
    "github.com/openai/openai-go/v3"
    "github.com/openai/openai-go/v3/live"
    "io"
    "os"
    "os/signal"
    "time"
)

func run() error {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    client := openai.NewClient()
    connection, err := client.Live.Connect(ctx, live.LiveConnectionOptions{})
    if err != nil {
        return err
    }
    defer connection.Abort()
    session := live.SessionConfigParam{
        Model:        "gpt-live-1",
        Instructions: openai.String("Be concise. Delegate requests needing current information to the backend, which can search the web."),
        Audio:        live.SessionConfigAudioParam{Format: live.AudioFormatParamOfAudioPCM(24000), Output: live.SessionConfigAudioOutputParam{Voice: live.SessionConfigAudioOutputVoiceUnionParam{OfString: openai.String("marin")}}},
        Delegation: live.SessionConfigDelegationUnionParam{OfResponses: &live.SessionConfigDelegationResponsesParam{Responses: live.ResponsesDelegationConfigParam{
            Model: "gpt-5.6-luna", Tools: []live.ResponsesDelegationConfigToolUnionParam{{OfWebSearch: &live.ResponsesDelegationConfigToolWebSearchParam{}}},
            ToolChoice: live.ResponsesDelegationConfigToolChoiceUnionParam{OfLiveToolChoiceEnum: openai.String("auto")},
        }}},
    }
    if err := connection.Send(ctx, live.ClientEventUnionParam{OfSessionStart: &live.SessionStartEventParam{Session: session, EventID: openai.String("event_start")}}); err != nil {
        return err
    }
    events := make(chan live.ServerEventUnion)
    failures := make(chan error, 2)
    go func() {
        for {
            event, err := connection.Recv(ctx)
            if err != nil {
                select {
                case failures <- err:
                case <-ctx.Done():
                }
                return
            }
            select {
            case events <- event:
            case <-ctx.Done():
                return
            }
        }
    }()
    audio := make(chan []byte)
    interrupts := make(chan os.Signal, 1)
    signal.Notify(interrupts, os.Interrupt)
    defer signal.Stop(interrupts)
    timer := time.NewTimer(30 * time.Second)
    defer timer.Stop()
    started, closing := false, false
    for {
        select {
        case <-timer.C:
            return fmt.Errorf("session startup or finalization timed out")
        case err := <-failures:
            return err
        case <-interrupts:
            if !started {
                return fmt.Errorf("interrupted before session.started")
            }
            if closing {
                continue
            }
            closing = true
            timer.Reset(15 * time.Second)
            if err := connection.Send(ctx, live.ClientEventUnionParam{OfSessionClose: &live.SessionCloseEventParam{}}); err != nil {
                return err
            }
        case chunk := <-audio:
            if closing {
                continue
            }
            if err := connection.Send(ctx, live.ClientEventUnionParam{OfSessionInputAudioAppend: &live.InputAudioAppendEventParam{Audio: base64.StdEncoding.EncodeToString(chunk)}}); err != nil {
                return err
            }
        case event := <-events:
            switch event.Type {
            case "error":
                return fmt.Errorf("Live error: %s", event.RawJSON())
            case "session.started":
                if started {
                    continue
                }
                started = true
                timer.Stop()
                fmt.Fprintln(os.Stderr, "Session ready", event.AsSessionStartedEvent().Session.ID)
                // stdin/stdout carry raw mono PCM16 at 24 kHz, not WAV files.
                go func() {
                    pending := []byte{}
                    for {
                        buffer := make([]byte, 4800)
                        n, err := os.Stdin.Read(buffer)
                        pending = append(pending, buffer[:n]...)
                        complete := len(pending) - len(pending)%2
                        if complete > 0 {
                            chunk := append([]byte(nil), pending[:complete]...)
                            pending = pending[complete:]
                            select {
                            case audio <- chunk:
                            case <-ctx.Done():
                                return
                            }
                        }
                        if err != nil {
                            if err != io.EOF {
                                select {
                                case failures <- err:
                                case <-ctx.Done():
                                }
                            }
                            // EOF does not close the session. Press Ctrl+C to finalize it.
                            return
                        }
                    }
                }()
            case "session.output_audio.delta":
                chunk, err := base64.StdEncoding.DecodeString(event.AsOutputAudioDeltaEvent().Delta)
                if err != nil {
                    return err
                }
                if _, err = os.Stdout.Write(chunk); err != nil {
                    return err
                }
            case "session.closed":
                if !started {
                    return fmt.Errorf("session closed before startup")
                }
                fmt.Fprintln(os.Stderr, "Final session usage", event.AsSessionClosedEvent().Usage)
                return nil
            default:
                fmt.Fprintln(os.Stderr, event.RawJSON())
            }
        }
    }
}
func main() {
    if err := run(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

Python 的 websocket-client:

来源示例 4(python):

沿用程序文字译释224。

程序文字译释261:收到事件。

// [中文] 把标准输入的24 kHz单声道PCM16音频持续送入Live连接,把返回音频写到标准输出;日志与转录走标准错误,关闭时等待会话结束。
import com.openai.client.okhttp.OkHttpClient;
import com.openai.core.ClientOptions;
import com.openai.helpers.LiveConnection;
import com.openai.models.live.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;

public final class LiveConnectionWebsocketExample {
  public static void main(String[] args) throws Exception {
    var session =
        SessionConfig.builder()
            .model("gpt-live-1")
            .instructions(
                "Be concise. Delegate requests needing current information to the backend, which can search the web.")
            .audio(
                SessionConfig.Audio.builder()
                    .audioPcmFormat(AudioFormat.AudioPcm.Rate.of(24000L))
                    .output(SessionConfig.Audio.Output.builder().voice("marin").build())
                    .build())
            .responsesDelegation(
                ResponsesDelegationConfig.builder()
                    .model("gpt-5.6-luna")
                    .addToolWebSearch()
                    .toolChoice(ResponsesDelegationConfig.ToolChoice.LiveToolChoiceEnum.AUTO)
                    .build())
            .build();
    var closing = new AtomicBoolean();
    var finished = new CountDownLatch(1);
    var audio = new ArrayBlockingQueue<byte[]>(8);
    var failures = new ArrayBlockingQueue<Exception>(1);
    try (var http = OkHttpClient.builder().build();
        var connection =
            LiveConnection.connect(ClientOptions.builder().fromEnv().httpClient(http).build())) {
      Thread shutdown =
          new Thread(
              () -> {
                closing.set(true);
                try {
                  if (!finished.await(15, TimeUnit.SECONDS)) {
                    System.err.println("Incomplete finalization: no session.closed event");
                    connection.close();
                  }
                } catch (InterruptedException interrupted) {
                  Thread.currentThread().interrupt();
                }
              });
      Runtime.getRuntime().addShutdownHook(shutdown);
      try {
        connection.send(
            ClientEvent.ofSessionStart(
                SessionStartEvent.builder().session(session).eventId("event_start").build()));
        boolean started = false, closeSent = false;
        long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(30);
        var incoming = connection.receiveAsync();
        while (true) {
          if (System.nanoTime() > deadline)
            throw new IllegalStateException("Session startup or finalization timed out");
          if (!failures.isEmpty()) throw failures.take();
          if (closing.get() && started && !closeSent) {
            closeSent = true;
            deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(15);
            connection.send(ClientEvent.ofSessionClose(SessionCloseEvent.builder().build()));
          }
          var chunk = audio.poll();
          if (chunk != null && !closing.get())
            connection.send(
                ClientEvent.ofSessionInputAudioAppend(
                    InputAudioAppendEvent.builder()
                        .audio(Base64.getEncoder().encodeToString(chunk))
                        .build()));
          ServerEvent event;
          try {
            event = incoming.get(20, TimeUnit.MILLISECONDS);
          } catch (TimeoutException waiting) {
            continue;
          }
          if (event.isErrorEvent()) throw new IllegalStateException(event.toString());
          if (event.isSessionStarted() && !started) {
            started = true;
            deadline = Long.MAX_VALUE;
            System.err.println("Session ready " + event.asSessionStarted().session().id());
            // stdin/stdout carry raw mono PCM16 at 24 kHz, not WAV files.
            Thread reader =
                new Thread(
                    () -> {
                      try {
                        int pending = -1;
                        while (!closing.get()) {
                          byte[] buffer = new byte[4800];
                          int offset = pending < 0 ? 0 : 1;
                          if (offset == 1) buffer[0] = (byte) pending;
                          int count = System.in.read(buffer, offset, buffer.length - offset);
                          // EOF does not close the session. Press Ctrl+C to finalize it.
                          if (count < 0) return;
                          count += offset;
                          pending = count % 2 == 0 ? -1 : Byte.toUnsignedInt(buffer[count - 1]);
                          int complete = count - count % 2;
                          if (complete > 0) audio.put(Arrays.copyOf(buffer, complete));
                        }
                      } catch (Exception error) {
                        failures.offer(error);
                      }
                    });
            reader.setDaemon(true);
            reader.start();
          } else if (event.isSessionOutputAudioDelta()) {
            System.out.write(Base64.getDecoder().decode(event.asSessionOutputAudioDelta().delta()));
            System.out.flush();
          } else if (event.isSessionClosed()) {
            if (!started) throw new IllegalStateException("Session closed before startup");
            System.err.println("Final session usage " + event.asSessionClosed().usage());
            return;
          } else {
            System.err.println(event);
          }
          incoming = connection.receiveAsync();
        }
      } finally {
        closing.set(true);
        finished.countDown();
        try {
          Runtime.getRuntime().removeShutdownHook(shutdown);
        } catch (IllegalStateException shuttingDown) {
          /* The hook is already draining. */
        }
      }
    }
  }
}

Ruby OpenAI SDK:先执行 gem install openai async-websocket。

来源示例 5(ruby):

程序文字译释262:已连接Realtime API,后接主机名。

程序文字译释263:已收到事件,后接类型。

## [中文] 把标准输入的24 kHz单声道PCM16音频持续送入Live连接,把返回音频写到标准输出;日志与转录走标准错误,关闭时等待会话结束。
require "openai"
require "base64"
$stdin.binmode
$stdout.binmode
session = {
  model: "gpt-live-1",
  instructions: "Be concise. Delegate requests needing current information to the backend, which can search the web.",
  audio: {
    format: {
      type: :"audio/pcm",
      rate: 24_000
    },
    output: { voice: :marin }
  },
  delegation: {
    type: :responses,
    responses: {
      model: "gpt-5.6-luna",
      tools: [{ type: :web_search }],
      tool_choice: :auto
    }
  }
}
client = OpenAI::Client.new
client.live.connect do |connection|
  state = {
    closing: false,
    finalized: false,
    started: false
  }
  threads = {}
  mutex = Mutex.new
  previous = trap("INT") do
    threads[:closer] ||= Thread.new do
      mutex.synchronize do
        state[:closing] = true
        if state[:started]
          connection.send_event(type: :"session.close")
        else
          connection.close
        end
      end
      sleep 15
      unless state[:finalized]
        warn "Incomplete finalization: no session.closed event"
        connection.close
      end
    end
  end
  begin
    threads[:startup] = Thread.new do
      sleep 30
      unless state[:started]
        warn "Session startup timed out"
        connection.close
      end
    end
    connection.send_event(type: :"session.start", session: session, event_id: "event_start")
    connection.each do |event|
      case event
      when OpenAI::Models::Live::ErrorEvent
        raise "Live error: #{event.to_json}"
      when OpenAI::Models::Live::SessionStartedEvent
        next if state[:started]

        state[:started] = true
        warn "Session ready #{event.session.id}"
        # stdin/stdout carry raw mono PCM16 at 24 kHz, not WAV files.
        threads[:sender] = Thread.new do
          pending = +"".b
          loop do
            pending << $stdin.readpartial(4800)
            complete = pending.bytesize - pending.bytesize % 2
            chunk = pending.slice!(0, complete).to_s
            mutex.synchronize do
              break if state[:closing]

              connection.send_event(type: :"session.input_audio.append", audio: Base64.strict_encode64(chunk)) unless chunk.empty?
            end
          end
        rescue EOFError
          # EOF does not close the session. Press Ctrl+C to finalize it.
          nil
        rescue => error
          warn "Audio input failed: #{error.class}"
          connection.close
        end
      when OpenAI::Models::Live::OutputAudioDeltaEvent
        $stdout.write(Base64.strict_decode64(event.delta))
        $stdout.flush
      when OpenAI::Models::Live::SessionClosedEvent
        raise "Session closed before startup" unless state[:started]

        warn "Final session usage #{event.usage}"
        state[:finalized] = true
        break
      else
        warn event.to_json
      end
    end
    raise "Connection closed without final session usage" unless state[:finalized]
  ensure
    trap("INT", previous)
    threads[:startup]&.kill
    threads[:startup]&.join
    threads[:sender]&.kill
    threads[:sender]&.join
    threads[:closer]&.kill
    threads[:closer]&.join
  end
end

// [中文] 用本语言客户端连接指定WebSocket端点,区分Live与Realtime的凭证、URL和事件;应用继续负责音频收发与会话生命周期。
import WebSocket from "ws";

const url = "wss://api.openai.com/v1/realtime?model=gpt-realtime-2.1";
const ws = new WebSocket(url, {
  headers: {
    Authorization: "Bearer " + process.env.OPENAI_API_KEY,
    "OpenAI-Safety-Identifier": "hashed-user-id",
  },
});

let connected = false;
ws.on("message", function incoming(message) {
  const event = JSON.parse(message.toString());
  if (event.type === "error") throw new Error(event.error.message);
  if (event.type === "session.created") {
    connected = true;
    console.log(`Connected to the Realtime API: ${event.session.id}`);
    ws.close(1000);
  }
});
ws.on("close", () => {
  if (!connected)
    throw new Error("Connection closed before the session was created");
});

## [中文] 用本语言客户端连接指定WebSocket端点,区分Live与Realtime的凭证、URL和事件;应用继续负责音频收发与会话生命周期。
## example requires websocket-client library:
## pip install websocket-client

import os
import json
import websocket

OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]

url = "wss://api.openai.com/v1/realtime?model=gpt-realtime-2.1"
headers = [
    "Authorization: Bearer " + OPENAI_API_KEY,
    "OpenAI-Safety-Identifier: hashed-user-id",
]


session_created = False


def on_message(ws, message):
    global session_created
    data = json.loads(message)
    if data["type"] == "session.created":
        session_created = True
        print("Connected to the Realtime API:", data["session"]["id"])
        ws.close()
    elif data["type"] == "error":
        ws.close()


ws = websocket.WebSocketApp(
    url,
    header=headers,
    on_message=on_message,
)

ws.run_forever()
if not session_created:
    raise RuntimeError("Connection closed before the Realtime session was created.")

## [中文] 用本语言客户端连接指定WebSocket端点,区分Live与Realtime的凭证、URL和事件;应用继续负责音频收发与会话生命周期。
require "openai"

client = OpenAI::Client.new(
  default_headers: { "OpenAI-Safety-Identifier" => "hashed-user-id" }
)

client.realtime.connect(model: "gpt-realtime-2.1") do |connection|
  created = connection.find do |event|
    raise event.error.message if event.is_a?(OpenAI::Realtime::RealtimeErrorEvent)

    next unless event.is_a?(OpenAI::Realtime::SessionCreatedEvent)

    puts("Connected to the Realtime API: #{event.session[:id]}")
    true
  end
  raise "Connection closed before the session was created" unless created
end

Java OpenAI SDK:Java 17 或更高,依赖 com.openai:openai-java:4.75.1。

来源示例 6(java):

沿用程序文字译释224。

// [中文] 示例 6:通过 WebSocket 连接;以下保留本语言原始示例。
// [中文] 原代码明确设置的模型值:gpt-realtime-2.1;各值的作用按其所在配置对象区分。
// [中文] 沿原示例执行这些方法:connection.receive;异步调用及事件接收顺序保留。
import com.openai.client.okhttp.OkHttpClient;
import com.openai.core.ClientOptions;
import com.openai.helpers.RealtimeConnection;
import com.openai.helpers.RealtimeWebSocketOptions;
import com.openai.models.realtime.*;

var http = OkHttpClient.builder().build();
var options =
    ClientOptions.builder()
        .fromEnv()
        .httpClient(http)
        .putHeader("OpenAI-Safety-Identifier", "hashed-user-id")
        .build();
try (http;
    var connection =
        RealtimeConnection.connect(
            options,
            RealtimeWebSocketOptions.builder().model("gpt-realtime-2.1").build())) {
  while (true) {
    var event = connection.receive();
    if (event.error().isPresent())
      throw new IllegalStateException(event.error().orElseThrow().toString());
    if (event.sessionCreated().isPresent()) {
      System.out.println("Connected to server.");
      System.out.println(event);
      break;
    }
  }
}

// [中文] 用本语言客户端连接指定WebSocket端点,区分Live与Realtime的凭证、URL和事件;应用继续负责音频收发与会话生命周期。
// [中文] 本段为配置或调用片段,需结合外层程序与已建立连接使用;不作为独立完整程序宣称编译通过。
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
client := openai.NewClient()
conn, err := client.Realtime.Connect(ctx, realtime.RealtimeConnectionOptions{}, option.WithQuery("model", "gpt-realtime-2.1"), option.WithHeader("OpenAI-Safety-Identifier", "hashed-user-id"))
if err != nil {
    return err
}
defer conn.Close()

.NET OpenAI SDK:执行 dotnet add package OpenAI --version 2.14.0。Realtime 使用实验性 SDK 类型,示例包含必要警告指令。

来源示例 7(csharp):

沿用程序文字译释224。

程序文字译释264:会话创建前连接已关闭。

// [中文] 示例 7:通过 WebSocket 连接;以下保留本语言原始示例。
// [中文] 沿原示例执行这些方法:client.StartConversationSessionAsync、session.ReceiveUpdatesAsync;异步调用及事件接收顺序保留。
using OpenAI.Realtime;

#pragma warning disable OPENAI002

string key = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!;
RealtimeClient client = new(key);

RealtimeSessionClientOptions options = new();
options.Headers["OpenAI-Safety-Identifier"] = "hashed-user-id";
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(30));
using RealtimeSessionClient session = await client.StartConversationSessionAsync("gpt-realtime-2.1", options, cancellationToken: timeout.Token);
await foreach (RealtimeServerUpdate update in session.ReceiveUpdatesAsync(timeout.Token))
{
    if (update is RealtimeServerUpdateError error)
        throw new InvalidOperationException(error.Error.Message);
    if (update is RealtimeServerUpdateSessionCreated)
    {
        Console.WriteLine("Connected to server.");
        Console.WriteLine(System.ClientModel.Primitives.ModelReaderWriter.Write(update));
        return;
    }
}
throw new InvalidOperationException("Connection closed before session creation.");

浏览器标准 WebSocket:

来源示例 8(javascript):

沿用程序文字译释224。

// [中文] 用本语言客户端连接指定WebSocket端点,区分Live与Realtime的凭证、URL和事件;应用继续负责音频收发与会话生命周期。
/*
Note that in client-side environments like web browsers, we recommend
using WebRTC instead. It is possible, however, to use the standard
WebSocket interface in browser-like environments like Deno and
Cloudflare Workers.
*/

const ws = new WebSocket(
  "wss://api.openai.com/v1/realtime?model=gpt-realtime-2.1",
  [
    "realtime",
    // Use a short-lived token fetched from your application server.
    "openai-insecure-api-key." + OPENAI_REALTIME_EPHEMERAL_KEY,
  ]
);

ws.addEventListener("open", function open() {
  console.log("Connected to server.");
});

ws.addEventListener("message", function incoming(event) {
  console.log(event.data);
});

发送与接收事件

会话由开发者客户端事件及 API 的服务端事件共同管理。

WebSocket 以文本字符串形式收发 JSON 序列化事件。以下 Node.js 原理同样适用于其他库:

来源示例 9(javascript):

沿用程序文字译释224。

程序文字译释225:今天格外友善一些!

// [中文] 示例 9:发送与接收事件;以下保留本语言原始示例。
// [中文] 本段构造的事件类型:session.update;不要将构造载荷等同于收到成功确认。
// [中文] 沿原示例执行这些方法:ws.on、ws.send;异步调用及事件接收顺序保留。
import WebSocket from "ws";

const url = "wss://api.openai.com/v1/realtime?model=gpt-realtime-2.1";
const ws = new WebSocket(url, {
  headers: {
    Authorization: "Bearer " + process.env.OPENAI_API_KEY,
    "OpenAI-Safety-Identifier": "hashed-user-id",
  },
});

ws.on("open", function open() {
  console.log("Connected to server.");

  // Send client events over the WebSocket once connected
  ws.send(
    JSON.stringify({
      type: "session.update",
      session: {
        type: "realtime",
        instructions: "Be extra nice today!",
      },
    })
  );
});

// Listen for and parse server events
ws.on("message", function incoming(message) {
  console.log(JSON.parse(message.toString()));
});

WebSocket 可能是实时模型最底层的交互接口,音频 Base64 编码块的发送与处理均由应用负责。收发音频见 Realtime 对话指南。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号