本文档面向客户端接入方,描述如何通过 WebSocket 协议接入本服务的端到端实时语音对话能力(Speech-to-Speech, S2S),包含 URL 规范、鉴权流程、全量事件协议、交互时序、错误码与常见问题等。
说明:
本服务对外协议100%兼容 Realtime WebSocket GA 事件规范,按标准 Realtime GA SDK 编写的客户端可以直接接入。
服务端内部提供
audio-og-realtime-2.1 / audio-og-realtime-2.1mini / audio-s-realtime-3.0 三个能力系列,运行时根据客户请求自动选路。1. 服务概述
通信协议:WebSocket (RFC 6455) over TLS,
wss:// 生产强制、ws:// 仅内网调试。消息格式:
Text 帧承载 JSON 事件,Binary 帧不接受(会被服务端忽略并打 warn)。音频承载:所有音频均以 base64 字符串放在 JSON 事件的
audio / delta 字段。支持的能力系列:
model 参数 | 定位 |
audio-og-realtime-2.1 | 端到端语音对话 · 基础能力。 |
audio-og-realtime-2.1mini | 端到端语音对话 · 轻量型。 |
audio-s-realtime-3.0 | 端到端语音对话 · 增强能力。 |
说明:
客户端可以任意大小写传入(
Audio-OG-realtime-2.1 与 audio-og-realtime-2.1 等价),服务端在解析时统一归一为小写。规范值为上表中的小写形式,本文档下同。统一协议:不同能力系列使用完全相同的 WebSocket 事件协议(与标准 openai Realtime 一致)。客户端无需为不同
model 编写分支代码;能力差异由服务端内部屏蔽,不支持的字段会静默忽略或在控制事件上以error(code=not_supported) 回复,不会中断会话。单会话最大空闲超时:默认30秒,可通过
timeoutSec 查询参数覆盖,上限120秒。未指定
model 时的兜底:服务端根据账号维度的默认路由做兜底,仍未命中时使用 audio-og-realtime-2.1。2. 接入 URL 与查询参数
2.1 URL 模板
wss://{host}/ete/v1/{appid}?model=<audio-og-realtime-2.1|audio-og-realtime-2.1mini|audio-s-realtime-3.0>&secretId=<TC3 SecretId>&signature=<TC3-HMAC-SHA256 hex>&timeStamp=<unix 秒>&expired=<unix 秒>&nonce=<随机整数>&timeoutSec=<会话空闲超时秒>
2.2 路径参数
参数 | 必填 | 说明 |
appid | 是 | 腾讯云 AppID,十进制无符号整数,随路径 /ete/v1/{appid} 传入。 |

2.3 其他参数(Query)
参数 | 必填 | 类型 | 说明 |
secretId | 是 | string | CAM SecretId。 |
signature | 是 | string | |
timeStamp | 是 | int64 | 签名时的当前 UNIX 时间戳(秒)。 |
expired | 是 | int64 | 签名过期时间的 UNIX 时间戳(秒),必须 > timeStamp 且 > now。 |
nonce | 是 | int64 | 一次性随机整数(防重放)。 |
model | 否 | string | |
timeoutSec | 否 | int | 会话空闲超时(秒),默认30,最大120(超过上限会被截断到120)。 |
resId | 否 | string | 业务侧资源 ID,透传用于对账。 |
2.4 URL 示例
wss://mps.cloud.tencent.com/ete/v1/1301234567?model=audio-s-realtime-3.0&secretId=AKIDzxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&signature=8f2c...4b3a&timeStamp=1735012345&expired=1735015945&nonce=8391023472&timeoutSec=60&resId=order_20260826_0001
3. 签名
3.1 参与签名的查询参数
除
signature 本身外的所有 query 参数都参与签名(按字母升序排列)。这意味着:必填参数
secretId / timeStamp / expired / nonce 一定参与签名。可选参数(如
model / timeoutSec / resId)只要您在 URL 里带上了就必须参与签名;未带则不参与。signature 参数本身不参与签名(作为签名结果放在 URL 上)。其他业务参数(如音频格式、voice、instructions、tools 等)通过握手成功后的
session.update 事件传递,与 URL 无关。3.2 签名算法
Step 1: 排序并 URL-encode,构造规范化查询串keys = sort(参与签名的 params keys) // 除 signature 之外的所有 querycanonicalQueryString = keys.map(k => k + '=' + urlEncode(params[k])).join('&')Step 2: 构造规范化请求canonicalRequest ="post" + "\\n"+ "/ete/v1/{appid}" + "\\n"+ canonicalQueryString + "\\n"+ "content-type:application/json; charset=utf-8" + "\\n"+ "host:" + host + "\\n"+ "" + "\\n"+ "content-type;host" + "\\n"Step 3: 构造待签字符串date = UTC(timeStamp) 的 YYYY-MM-DDcredentialScope = date + "/mps/tc3_request"hashedRequest = hex( SHA-256( canonicalRequest ) )stringToSign = "TC3-HMAC-SHA256" + "\\n"+ timeStamp + "\\n"+ credentialScope + "\\n"+ hashedRequestStep 4: 计算派生签名密钥kDate = HMAC-SHA256( date, "TC3" + secretKey )kService = HMAC-SHA256( "mps", kDate )kSigning = HMAC-SHA256( "tc3_request", kService )Step 5: 计算最终签名signature = hex( HMAC-SHA256( stringToSign, kSigning ) ) // 小写 hex
3.3 JavaScript 参考实现
generateSignature() 方法,使用浏览器原生 crypto.subtle API 完成。以下节选核心片段:async function sign(cfg) {const now = Math.floor(Date.now() / 1000);const expired = now + 3600;const params = {// 除 signature 外的所有 query 参数, 均需参与签名model: cfg.model, // 可选; 未传则不放入 paramstimeStamp: String(now),expired: String(expired),nonce: String(Math.floor(Math.random() * 1e10)),secretId: cfg.secretId,timeoutSec: String(cfg.timeoutSec || 30),// 若使用 resId, 也一并放入并参与签名:// resId: cfg.resId,};const path = `/ete/v1/${cfg.appid}`;const keys = Object.keys(params).sort();const canonicalQueryString = keys.map((k) => `${k}=${encodeURIComponent(params[k])}`).join('&');const canonicalRequest = ['post', path, canonicalQueryString,'content-type:application/json; charset=utf-8',`host:${cfg.host}`, '', 'content-type;host', '',].join('\\n');const hashed = await sha256Hex(canonicalRequest);const date = new Date(now * 1000).toISOString().split('T')[0];const scope = `${date}/mps/tc3_request`;const stringToSign = ['TC3-HMAC-SHA256', String(now), scope, hashed].join('\\n');const kDate = await hmacSha256(date, 'TC3' + cfg.secretKey);const kService = await hmacSha256('mps', kDate);const kSigning = await hmacSha256('tc3_request', kService);const signature = bufToHex(await hmacSha256(stringToSign, kSigning));const q = new URLSearchParams(); keys.forEach((k) => q.append(k, params[k]));q.append('signature', signature);return `wss://${cfg.host}${path}?${q.toString()}`;}
3.4 Go 参考实现
func sign(host, appID, secretID, secretKey, model string, timeoutSec int) string {now := time.Now().Unix()expired := now + 3600params := map[string]string{"timeStamp": strconv.FormatInt(now, 10),"expired": strconv.FormatInt(expired, 10),"nonce": strconv.Itoa(rand.Intn(1e9)),"secretId": secretID,"timeoutSec": strconv.Itoa(timeoutSec),}if model != "" {params["model"] = model}keys := make([]string, 0, len(params))for k := range params { keys = append(keys, k) }sort.Strings(keys)parts := make([]string, 0, len(keys))for _, k := range keys {parts = append(parts, k+"="+url.QueryEscape(params[k]))}cqs := strings.Join(parts, "&")path := "/ete/v1/" + appIDcanonical := strings.Join([]string{"post", path, cqs,"content-type:application/json; charset=utf-8","host:" + host, "", "content-type;host", "",}, "\\n")hashed := sha256Hex([]byte(canonical))date := time.Unix(now, 0).UTC().Format("2006-01-02")scope := date + "/mps/tc3_request"stringToSign := strings.Join([]string{"TC3-HMAC-SHA256", strconv.FormatInt(now, 10), scope, hashed,}, "\\n")kDate := hmac256([]byte(date), []byte("TC3"+secretKey))kService := hmac256([]byte("mps"), kDate)kSigning := hmac256([]byte("tc3_request"), kService)signature := hex.EncodeToString(hmac256([]byte(stringToSign), kSigning))q := url.Values{}for k, v := range params { q.Set(k, v) }q.Set("signature", signature)return "wss://" + host + path + "?" + q.Encode()}func sha256Hex(b []byte) string { h := sha256.Sum256(b); return hex.EncodeToString(h[:]) }func hmac256(msg, key []byte) []byte { m := hmac.New(sha256.New, key); m.Write(msg); return m.Sum(nil) }
3.5 鉴权失败的握手响应
服务端在 WebSocket 握手升级前就完成鉴权与并发检查,任一步失败会以 HTTP 响应拒绝,不会进入 WebSocket 通道,因此客户端不会收到
error 事件,而是拿到一个标准的 HTTP 状态码。见 第 9.1 节。4. 交互时序
4.1 关键约定
握手成功后服务端不主动下发任何事件。首条消息必须由客户端发起:
客户端发
session.update 完成会话初始化,服务端才回一条 session.updated。收到
session.updated 才代表“可以开始送音频”。4.2 典型时序(服务端 VAD 模式)

4.3 打断当前 response
客户端可以随时发送
{"type": "response.cancel"} 打断当前正在生成的 response。服务端会立即停止 TTS 下发,并回一条
response.done(status 可能为 cancelled)。5. 上行事件(Client → Server)
5.1 事件类型速览
事件 type | 用途 |
session.update | 更新会话参数(首次调用等价于“完成握手初始化”)。 |
input_audio_buffer.append | 追加一段用户音频(base64 编码)。 |
input_audio_buffer.commit | 手动提交音频缓冲(关闭 server_vad 时使用)。 |
input_audio_buffer.clear | 清空未 commit 的上行音频缓冲。 |
response.create | 主动请求服务端生成一次 response(可携带 instructions 用作打招呼 / 干预回复)。 |
response.cancel | 打断当前正在生成的 response。 |
conversation.item.create | 向会话上下文新增一条 item(含 function_call_output 回传)。 |
conversation.item.retrieve | 请求获取指定 item 详情。 |
conversation.item.delete | 删除指定 item。 |
conversation.item.truncate | 截断已生成的 assistant 音频(部分场景不支持,会回 error(not_supported))。 |
output_audio_buffer.clear | 清空当前 assistant 音频播放缓冲(服务端会同步映射为 response.cancel)。 |
transcription_session.update | 独立 transcription 会话更新(当前不支持,会回 error(not_supported))。 |
5.2 session.update
{"type": "session.update","session": {"instructions": "你是一个专业的智能助手, 说话简洁友好。","audio": {"input": {"format": { "type": "audio/pcm", "rate": 16000 },"transcription": { "language": "zh" }},"output": {"format": { "type": "audio/pcm", "rate": 24000 },"voice": "xxxxxx","speed": 100,"loudness": 100}},"tools": [{"type": "function","name": "get_current_time","description": "获取当前系统时间","parameters": { "type": "object", "properties": {}, "required": [] }}]}}
字段说明:
字段 | 类型 | 说明 |
session.instructions | string | System instructions,覆盖服务端默认。 |
session.audio.input.format.type | string | |
session.audio.input.format.rate | int | 上行采样率(Hz)。 |
session.audio.input.transcription.language | string | ASR 转写语言(ISO 639-1,如 zh / en / ja)。留空则由服务端自动检测;部分能力基于内建语言自适应,客户端指定值可能会被忽略。 |
session.audio.output.format.type | string | 下行音频格式。 |
session.audio.output.format.rate | int | 下行采样率(Hz)。 |
session.audio.output.voice | string | |
session.audio.output.speed | int | 语速(部分能力支持,不支持时静默忽略)。 |
session.audio.output.loudness | int | 响度(部分能力支持,不支持时静默忽略)。 |
session.tools | array | Function tool 定义,遵循标准 GA Function Tool schema。 |
session.extension | object | 预留的高级拓展字段(部分能力支持,不支持时静默忽略)。 |
VAD / turn_detection:服务端默认启用
server_vad。客户端在 session.audio.input.turn_detection 里的自定义参数,服务端会尽力适配;具体生效情况以 session.updated.session.audio.input.turn_detection 回执为准。5.3 input_audio_buffer.append
{"type": "input_audio_buffer.append","audio": "<base64-encoded audio chunk>"}
audio 是一段完整的音频负载,服务端按 session.audio.input.format 声明的格式解码。推荐分片大小:20ms-40ms(PCM 16k 单声道下约 640~1280 字节 base64 前)。
首次
session.update 之前送入的 append 会被静默丢弃。5.4 input_audio_buffer.commit / clear
{ "type": "input_audio_buffer.commit" }{ "type": "input_audio_buffer.clear" }
commit 仅在关闭 server_vad 时需要显式调用。clear 用于取消当前未提交的用户音频,无论能力后端是否原生支持,服务端都会回一条 input_audio_buffer.cleared 事件给客户端。5.5 response.create / cancel
{"type": "response.create","response": {"instructions": "你好, 我是你的智能助手。" // 可选}}{ "type": "response.cancel" }
统一语义(各
model 行为一致):带
response.instructions → 让服务端主动说这段话(打招呼 / 干预回复)。不带
instructions → 尝试触发一次新的 response 生成。部分能力在等待生成上下文时会回复 error(code=not_supported),客户端可忽略此提示或改发带 instructions 的版本。response.create 支持的字段:字段 | 类型 | 说明 |
response.instructions | string | 让服务端主动说出的文本。 |
response.voice | string | 本轮临时覆盖的音色(可选)。 |
response.modalities | []string | 本轮输出模态(可选,标准)。 |
response.metadata | object | 透传给服务端记录用的元数据(可选)。 |
5.6 conversation.item.create
用途 1:追加 user / assistant message
{"type": "conversation.item.create","item": {"type": "message","role": "user","content": [{ "type": "input_text", "text": "帮我查一下深圳今天的天气" }]}}
{"type": "conversation.item.create","item": {"type": "function_call_output","call_id": "call_abc123","output": "2026-08-26 09:30:00"}}
5.7 conversation.item.retrieve / delete / truncate
{ "type": "conversation.item.retrieve", "item_id": "item_xxx" }{ "type": "conversation.item.delete", "item_id": "item_xxx" }{ "type": "conversation.item.truncate", "item_id": "item_xxx","content_index": 0, "audio_end_ms": 1500 }
注意:
conversation.item.truncate 在部分能力下不支持,服务端会以 error(code=not_supported) 回复;客户端可以改用 response.cancel +5.8 output_audio_buffer.clear
{ "type": "output_audio_buffer.clear" }
服务端会:
1. 内部触发一次
response.cancel 打断当前 TTS。2. 回一条
output_audio_buffer.cleared,客户端据此清空本地音频播放缓冲。6. 下行事件(Server → Client)
6.1 完整事件清单
事件 type | 触发时机 |
session.updated | 服务端应用 session.update 后的首条响应。 |
error | 参数错误 / 服务后端异常 / 不支持的能力。 |
conversation.item.created | 一条 item 加入会话上下文。 |
conversation.item.retrieved | 响应 conversation.item.retrieve。 |
conversation.item.deleted | 响应 conversation.item.delete。 |
conversation.item.input_audio_transcription.delta | 用户 ASR 增量转写(非稳态)。 |
conversation.item.input_audio_transcription.completed | 用户 ASR 稳态转写(可能携带 usage)。 |
conversation.item.input_audio_transcription.failed | 用户 ASR 转写失败。 |
input_audio_buffer.committed | 用户一句话音频入缓冲(VAD 触发或客户端 commit)。 |
input_audio_buffer.cleared | 响应 input_audio_buffer.clear。 |
input_audio_buffer.speech_started | 服务端 VAD 检测到用户开始说话。 |
input_audio_buffer.speech_stopped | 服务端 VAD 检测到用户停止说话。 |
response.created | 一次 response 开始生成。 |
response.output_item.added | response 内新增一个 output_item( message / function_call)。 |
response.content_part.added | output_item 内新增一个 content part( audio / text)。 |
response.output_audio.delta | 助手音频增量(base64)。 |
response.output_audio.done | 助手音频结束。 |
response.output_audio_transcript.delta | 助手音频伴随文本增量。 |
response.output_audio_transcript.done | 助手音频伴随文本稳态。 |
response.output_text.delta / response.output_text.done | 纯文本模态输出(预留,默认音频模态不下发)。 |
response.function_call_arguments.delta | Function call 参数增量(若能力后端按流式下发)。 |
response.function_call_arguments.done | Function call 参数完整下发。 |
response.content_part.done | content part 结束。 |
response.output_item.done | output_item 结束。 |
response.done | 一次 response 全部结束(可能携带 usage)。 |
output_audio_buffer.started | 服务端开始下发助手音频。 |
output_audio_buffer.stopped | 服务端一段助手音频结束。 |
output_audio_buffer.cleared | 响应客户端 output_audio_buffer.clear。 |
rate_limits.updated | 能力后端速率限额更新(若能力后端下发则透传)。 |
6.2 session.updated(服务端首条事件)
{"type": "session.updated","event_id": "evt_xxxxxxxx","session": {"id": "sess_xxxxxxxx","type": "realtime","model": "audio-s-realtime-3.0","audio": { "input": {...}, "output": {...} },"voice": "xxxxxx","instructions": "..."}}
客户端应在收到此事件之后才开始送
input_audio_buffer.append。事件中的 session.model 是本次会话实际生效模型名(可能来自客户端 URL 参数,也可能来自服务端默认路由兜底)。6.3 用户 ASR 事件
{"type": "conversation.item.input_audio_transcription.delta","event_id": "evt_xxx", "item_id": "item_xxx","content_index": 0, "delta": "深圳"}{"type": "conversation.item.input_audio_transcription.completed","event_id": "evt_xxx", "item_id": "item_xxx","content_index": 0, "transcript": "深圳今天天气怎么样","usage": { "type": "duration", "seconds": 2 }}
usage 字段承载本次 ASR 转写的按秒计费用量(type="duration" + seconds)。部分能力会携带此字段,其余情况缺省(omitempty);客户端应做字段可选兼容。
transcription.completed.usage 与 response.done.response.usage(token 用量)是独立并列的两个计费维度:ASR 按音频转写秒数计费、response 按 token 用量计费。6.4 助手音频 / 文本事件
{"type": "response.output_audio.delta","event_id": "evt_xxx","response_id": "resp_xxx","item_id": "item_xxx","output_index": 0, "content_index": 0,"delta": "<base64 pcm16 / opus chunk>"}{"type": "response.output_audio_transcript.delta","event_id": "evt_xxx","response_id": "resp_xxx","item_id": "item_xxx","delta": "深圳"}
6.5 response.done
{"type": "response.done","event_id": "evt_xxx","response": {"id": "resp_xxx","status": "completed","usage": {"total_tokens": 123,"input_tokens": 45,"output_tokens": 78,"input_token_details": {"text_tokens": 20,"audio_tokens": 15,"cached_tokens": 10,"cached_tokens_details": {"text_tokens": 6,"audio_tokens": 4}},"output_token_details": {"text_tokens": 30,"audio_tokens": 48}}}}
status 常见取值:completed / cancelled / failed / incomplete。response.usage 承载本轮 response 的 token 用量明细,字段结构与标准 Realtime GA 100% 对齐;能力后端未返回 usage 时该字段缺省(omitempty)。客户端可用于成本观测或计费自查。6.6 error
{"type": "error","event_id": "evt_xxx","error": {"type": "invalid_request_error","code": "unsupported_output_format","message": "unsupported opus output rate 44100, allowed: 8000/12000/16000/24000/48000","event_id": "客户端触发本 error 的原事件 event_id"}}
error 之后如果错误致命,服务端可能会主动关闭连接(例如
pipeline_init_failed / upstream_send_error / upstream_closed / idle_timeout),客户端应做相应处理。7. 音频格式与采样率
7.1 上行音频(Input)
格式( format.type) | 说明 | 允许的采样率 |
pcm / audio/pcm / pcm16 / pcm_s16le / s16le | 16-bit signed LE mono PCM | 任意(推荐 16000) |
opus / audio/opus | 裸 Opus packet(每个 append 携带一个完整 packet) | 8k / 12k / 16k / 24k / 48k |
7.2 下行音频(Output)
格式 | 允许的采样率 | 备注 |
pcm | 任意 | 服务端会按需做重采样至客户端声明的采样率。 |
opus | 8k / 12k / 16k / 24k / 48k | 服务端负责编码为裸 Opus packet;每个 response.output_audio.delta.delta 通常对应一个 20 ms Opus 帧。 |
兼容 MIME 别名:客户端可以用
audio/pcm / pcm / pcm16 / pcm_s16le 任一形式声明 PCM;用 audio/opus / opus 声明 Opus。服务端会自动归一化。7.3 服务端下行链路策略
服务端在会话建立时会根据客户端声明的
session.audio.output.format 自动选择最优下行链路(直接透传或服务端重采样 / 格式转换),客户端无感知。客户端只需保证
format / rate 落在 7.1 / 7.2 表格声明的合法范围内;实际生效的格式会在 session.updated.session.audio 里给出。8. Function Call 完整流程
8.1 时序

8.2 关键字段
下行
response.function_call_arguments.done:{"type": "response.function_call_arguments.done","event_id": "evt_xxx","response_id": "resp_xxx","item_id": "item_xxx","call_id": "call_abc123","name": "get_current_time","arguments": "{}"}
上行
conversation.item.create 回传:{"type": "conversation.item.create","item": {"type": "function_call_output","call_id": "call_abc123","output": "2026-08-26 09:30:00"}}
8.3 完整可运行示例
勾选左侧 “启用 Demo Tool” 复选框。
提问 “现在几点了?”。
观察运行日志中完整的 function call 时序。
9. 错误码与握手拒绝
9.1 握手阶段的 HTTP 拒绝码
在 WebSocket 升级完成前的鉴权与并发检查阶段,任一失败会以 HTTP 状态码拒绝:
HTTP 状态 | Reason Phrase | 触发原因 |
400 | invalid_request: ... | URL 参数缺失 / appid 非法 / model 值非法。 |
401 | invalid_timestamp | timeStamp 或 expired 已过期或格式错误。 |
401 | authentication_failed: code=<n> | CAM 签名校验失败。 |
403 | account_invalid | 账户欠费 / 冻结。 |
429 | connection_limit_exceeded: uin=... num=... max=... | 并发连接数超上限。 |
500 | authentication_failed: <err> | CAM 后端调用异常。 |
500 | check_user_status_error: <err> | 账户状态查询异常。 |
500 | limit_check_error: <err> | 并发限流查询异常。 |
客户端应根据这些状态码提示用户重试 / 联系管理员。
9.2 会话阶段的 error 事件
握手成功后,业务错误通过 JSON
error 事件下发:error.type | error.code | 说明 |
invalid_request_error | invalid_json | 消息不是合法 JSON。 |
invalid_request_error | invalid_audio | 上行音频 base64 解码或解码器解析失败。 |
invalid_request_error | unsupported_output_format | 下行 format 非 pcm / opus,或 opus 采样率不在白名单。 |
invalid_request_error | not_supported | 使用了当前会话能力下不支持的控制事件(如 conversation.item.truncate / transcription_session.update / 不带 instructions 的空 response.create)。 |
invalid_request_error | session_not_ready | 未先发 session.update 就送控制事件(response.create / conversation.item.* 等)。 |
invalid_request_error | missing_item | conversation.item.create 缺 item 字段。 |
invalid_request_error | missing_item_id | conversation.item.retrieve / delete 缺 item_id 字段。 |
server_error | pipeline_init_failed | 服务端会话初始化失败。 |
server_error | upstream_send_error | 服务端向能力后端发送数据失败。 |
server_error | upstream_error | 能力后端返回异常。 |
server_error | upstream_closed | 能力后端主动关闭。 |
session_expired | idle_timeout | 会话空闲超时(默认 30 s,可通过 timeoutSec 覆盖)。 |
10. 常见问题(FAQ)
Q1: 为什么建连成功后什么事件都没有收到?
本服务采用“客户端主导”模式,握手成功后服务端不主动下发任何事件。
客户端必须先发一条
session.update 完成会话初始化,服务端才回 session.updated(作为“首次可见响应”),此后才能开始送音频。这与标准 GA 规范中 session.created 先行的行为有意不同,为的是更符合“客户端主动”的语义。Q2: 客户端签名总是 401,如何排查?
优先检查:
1. 本机时钟偏差:TC3 允许
timeStamp 与服务器差异不超过 15 分钟,先用 date -u 校准机器时间。2. 参与签名的参数集:除
signature 外的所有 query 参数都要参与签名。timeoutSec / model / resId 只要出现在 URL 上就必须一并进入 canonicalQueryString。3. 参数排序:keys 必须按字母升序,然后逐个
k=urlEncode(v) 用 & 拼接。4. canonicalRequest 空行:
content-type;host 之前必须有一个空行(\\n\\n)。5. date 使用 UTC:不要用本地时区。
Q3: 上行只能发 JSON 吗?为什么发 Binary 帧被忽略?
是的,本服务与标准 Realtime GA 规范保持一致,只接受 WebSocket
Text 帧承载的 JSON 事件。音频通过 input_audio_buffer.append 事件里的 audio 字段以 base64 承载。Q4: 如何让服务端主动打招呼?
在
session.updated 之后立即发:{ "type": "response.create","response": { "instructions": "你好, 我是智能助手, 请问需要什么帮助?" } }
所有
model 都支持这种“客户端指定文本,让服务端主动说出”的用法。Q5: 客户端下行想要 opus 而不是 pcm,怎么做?
在
session.update.audio.output.format 中声明:{ "format": { "type": "opus", "rate": 48000 } }
采样率必须是 Opus 白名单:
8000 / 12000 / 16000 / 24000 / 48000。返回的 response.output_audio.delta.delta 字段是裸 Opus packet 的 base64(一次 delta 通常对应一个20ms Opus 帧),客户端需用 Opus 解码器还原为 PCM。Q6: conversation.item.truncate 报 not_supported,如何截断已生成的助手音频?
当前会话若回复了
not_supported,说明实际生效的 model 未提供原生的 truncate 能力。推荐替代方案:打断当前 TTS:发
response.cancel。清空已下发但未播完的音频:发
output_audio_buffer.clear,服务端会一并下发 output_audio_buffer.cleared,客户端本地清空播放调度即可。Q7: 单次会话最长多久?
本服务不限制会话总时长,只做空闲超时:默认30秒无收发消息即断开。
可通过 URL 参数
timeoutSec 覆盖,上限120秒。心跳发法建议:客户端在语音间歇期偶发送20ms的静音 PCM input_audio_buffer.append 即可保活。Q8: 有 Python / Java / iOS / Android 的 SDK 吗?
本服务对外协议 100% 兼容 标准 Realtime GA WebSocket API,因此任何按标准 GA 规范编写的客户端 SDK 都可以直接接入,只需改动:
WebSocket URL 换成本服务的 URL。
增加 TC3-HMAC-SHA256 签名逻辑。
session.audio.output.format 使用本服务支持的格式。Q9: 如何使用加密音色 ID?和明文音色可以混用吗?
session.audio.output.voice 字段同时支持明文音色 ID 与加密音色 ID,服务端会自动识别并处理:明文音色 ID:直接使用。
加密音色 ID(由音色加密服务生成):服务端通过内部解密接口换回真实音色 ID 后再使用。
Fallback 策略:以下情况都会静默降级到服务端配置的默认音色,不会中断会话建立:
1. 服务端解析异常 / 超时(内部会重试一定次数,仍失败则 fallback)。
2. 明文音色 ID(服务端无需解密,直接使用)。
3. 音色 ID 不适用于当前
model(例如把一个 model 下的专用音色传给了另一个 model)。4. 音色 ID 不在当前
model 可用音色集内。因此客户端可以放心把任何合法音色 ID 直接填进去,服务端会尽力解析并给出最好的结果;只有当所有路径都失败时才用服务端默认音色。
Q10: response.done.response.usage 与 transcription.completed.usage 分别怎么计费?
两个字段承载两个独立并列的计费维度:
conversation.item.input_audio_transcription.completed.usage= 本次 ASR 转写的按秒用量(
type="duration" + seconds);部分能力会携带此字段,其余情况缺省。response.done.response.usage= 本轮 response 的按 token 用量明细(
total_tokens / input_tokens / output_tokens / *_token_details);能力后端未返回 usage 时该字段缺省。客户端可用于本地成本观测;实际计费以账单为准。