Step Audio 2
阶跃端到端语音模型,支持音色复刻、工具调用与联网搜索。
step-audio-2快速开始#
- 创建 API Key
在控制台 我的 Key 创建通用 API Key(明文只显示一次),并确认账户余额充足。
- 设置环境变量
export TOKENTOSEA_API_KEY="sk-relay_…",示例从环境变量读取密钥。 - 发送请求
以下为 Chat Completions · SSE 流式 示例;其他接口见全部调用示例。
export TOKENTOSEA_API_KEY="你的主站通用 API Key"
REQUEST_KEY="$(uuidgen)"
curl -N https://api.tokentosea.com/v1/chat/completions \
-H "Authorization: Bearer $TOKENTOSEA_API_KEY" \
-H "Idempotency-Key: $REQUEST_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "step-audio-2",
"messages": [
{
"role": "user",
"content": "请简短介绍一下你能帮助我完成什么。"
}
],
"max_tokens": 1024,
"modalities": [
"text"
],
"stream": true
}'
# 查询原请求;异步任务未结束时稍后继续查询,不要重复提交。
curl "https://api.tokentosea.com/v1/results/$REQUEST_KEY" \
-H "Authorization: Bearer $TOKENTOSEA_API_KEY"
# 返回 asset_id 后,获取短期有效的 COS 下载链接。
# curl "https://api.tokentosea.com/v1/assets/ASSET_ID/download-url" \
# -H "Authorization: Bearer $TOKENTOSEA_API_KEY"# pip install requests
# export TOKENTOSEA_API_KEY="你的主站通用 API Key"
import json
import os
import time
import uuid
import requests
API_BASE = "https://api.tokentosea.com"
API_KEY = os.environ["TOKENTOSEA_API_KEY"] # 控制台「我的 Key」创建的通用 API Key
REQUEST_KEY = str(uuid.uuid4()) # Idempotency-Key:重试或查询结果时复用同一个值
AUTH = {"Authorization": f"Bearer {API_KEY}"}
PENDING = {"planned", "sending", "streaming", "upstream_pending", "billing", "archiving"}
payload = {
"model": "step-audio-2",
"messages": [{"role": "user", "content": "请简短介绍一下你能帮助我完成什么。"}],
"max_tokens": 1024,
"modalities": ["text"],
"stream": True,
}
def show(data):
print(json.dumps(data, ensure_ascii=False, indent=2))
def wait_for_result():
"""查询原请求;异步任务未结束时稍后继续查询,不要重复提交。"""
while True:
result = requests.get(f"{API_BASE}/v1/results/{REQUEST_KEY}", headers=AUTH, timeout=30).json()
if result.get("status") not in PENDING:
return result
time.sleep(5)
def events(response):
"""逐行读取 SSE,返回每个 data 帧解析后的 JSON。"""
for raw in response.iter_lines():
line = raw.decode("utf-8")
if line.startswith("data:") and line[5:].strip() not in ("", "[DONE]"):
yield json.loads(line[5:])
try:
with requests.post(
f"{API_BASE}/v1/chat/completions",
headers={**AUTH, "Idempotency-Key": REQUEST_KEY},
json=payload,
stream=True,
timeout=1300,
) as response:
print(response.status_code, response.headers.get("X-Request-Id"))
if not response.headers.get("Content-Type", "").startswith("text/event-stream"):
show(response.json()) # 错误信息或异步受理信封
else:
for event in events(response):
# 思考模型的推理过程在 delta.reasoning_content 中
for choice in event.get("choices") or []:
print((choice.get("delta") or {}).get("content") or "", end="", flush=True)
if event.get("usage"):
print("\nusage:", event["usage"])
if event.get("error"):
print("\n错误:", event["error"])
except requests.RequestException as error:
# 连接中断时请求可能已被受理:用同一个 REQUEST_KEY 查询结果,不要重新提交。
print("\n请求中断:", error)
result = wait_for_result()
print("结果:", result.get("status"), "结算:", result.get("billing_status"), result.get("error") or "")// Node.js 18+,无需额外依赖。保存为 example.mjs 后运行:node example.mjs
// export TOKENTOSEA_API_KEY="你的主站通用 API Key"
import { randomUUID } from 'node:crypto';
const API_BASE = 'https://api.tokentosea.com';
const API_KEY = process.env.TOKENTOSEA_API_KEY; // 控制台「我的 Key」创建的通用 API Key
const REQUEST_KEY = randomUUID(); // Idempotency-Key:重试或查询结果时复用同一个值
const auth = { Authorization: `Bearer ${API_KEY}` };
const PENDING = new Set(['planned', 'sending', 'streaming', 'upstream_pending', 'billing', 'archiving']);
const payload = {
model: 'step-audio-2',
messages: [{ role: 'user', content: '请简短介绍一下你能帮助我完成什么。' }],
max_tokens: 1024,
modalities: ['text'],
stream: true,
};
const show = data => console.log(JSON.stringify(data, null, 2));
// 查询原请求;异步任务未结束时稍后继续查询,不要重复提交。
async function waitForResult() {
for (;;) {
const result = await fetch(`${API_BASE}/v1/results/${REQUEST_KEY}`, { headers: auth }).then(r => r.json());
if (!PENDING.has(result.status)) return result;
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
// 逐行读取 SSE,返回每个 data 帧解析后的 JSON。
async function* events(response) {
const decoder = new TextDecoder();
let buffer = '';
for await (const chunk of response.body) {
buffer += decoder.decode(chunk, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
const data = line.startsWith('data:') ? line.slice(5).trim() : '';
if (data && data !== '[DONE]') yield JSON.parse(data);
}
}
}
try {
const response = await fetch(`${API_BASE}/v1/chat/completions`, {
method: 'POST',
headers: { ...auth, 'Idempotency-Key': REQUEST_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
console.log(response.status, response.headers.get('x-request-id'));
if (!(response.headers.get('content-type') ?? '').startsWith('text/event-stream')) {
show(await response.json()); // 错误信息或异步受理信封
} else {
for await (const event of events(response)) {
// 思考模型的推理过程在 delta.reasoning_content 中
for (const choice of event.choices ?? []) process.stdout.write(choice.delta?.content ?? '');
if (event.usage) console.log('\nusage:', event.usage);
if (event.error) console.error('\n错误:', event.error);
}
}
} catch (error) {
// 连接中断时请求可能已被受理:用同一个 REQUEST_KEY 查询结果,不要重新提交。
console.error('\n请求中断:', error.message);
}
const result = await waitForResult();
console.log('结果:', result.status, '结算:', result.billing_status, result.error ?? '');能力与接口#
文本生成 多模态理解
下表只列出已在该型号上完成验收的接口、响应方式与输入类型;其他组合会返回 422(如 endpoint_not_verified、response_mode_not_verified、input_modality_not_verified)。
| 接口 | JSON | SSE 流式 | WebSocket | 已验证输入 |
|---|---|---|---|---|
Chat Completions/v1/chat/completions | 不支持 | 支持 | 不支持 | 文本 |
实时会话/v1/realtime | 不支持 | 不支持 | 支持 | 音频 |
可用系统音色(4 个)
wenrounansheng qingchunshaonv elegantgentle-female livelybreezy-female
价格#
官方价格以人民币(CNY)公布,核对日期 2026-10-19。TokenToSea 按官方价格计费、不加价;调用从账户余额(美元)扣除,人民币价格按请求受理时冻结的汇率换算为美元,金额保留 6 位小数并向上取整。下表适用于:标准服务。
| 计费项 | 单价 | 计量单位 |
|---|---|---|
| 输入 token | ¥10 | 每百万 token |
| 输出 token | ¥70 | 每百万 token |
| 缓存命中 token | ¥2 | 每百万 token |
每次请求先按可能的最高用量预留金额,完成后按供应商返回的实际用量结算并退回差额。计费方式详见计费与结算。
官方价格来源
限制#
| 项目 | 取值 | 说明 |
|---|---|---|
| 输出长度参数上限 | 131,072 token | 平台校验 max_tokens 等参数;实际可输出长度以模型为准 |
| 默认输出长度 | 4,096 token | 请求未指定输出长度参数时自动填入 |
响应方式:SSE 流式、WebSocket。
- 同一账户最多同时进行 8 个已预留或执行中的调用(平台规则)。
- JSON 请求体不超过 48 MB;multipart 上传仅 1 个
file,不超过 25 MB。 - 实时会话:单次最长 60 秒、最多 8 轮,排队中的输入音频不超过 4 MiB;断线后请查询结果,不要自动重连重新生成。
全部调用示例#
每个示例都包含 cURL、Python 与 Node.js 三种写法,发送完全相同的请求体,并用同一个 Idempotency-Key 查询结果。
Chat Completions · SSE 流式#
POST /v1/chat/completions 接口参考
export TOKENTOSEA_API_KEY="你的主站通用 API Key"
REQUEST_KEY="$(uuidgen)"
curl -N https://api.tokentosea.com/v1/chat/completions \
-H "Authorization: Bearer $TOKENTOSEA_API_KEY" \
-H "Idempotency-Key: $REQUEST_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "step-audio-2",
"messages": [
{
"role": "user",
"content": "请简短介绍一下你能帮助我完成什么。"
}
],
"max_tokens": 1024,
"modalities": [
"text"
],
"stream": true
}'
# 查询原请求;异步任务未结束时稍后继续查询,不要重复提交。
curl "https://api.tokentosea.com/v1/results/$REQUEST_KEY" \
-H "Authorization: Bearer $TOKENTOSEA_API_KEY"
# 返回 asset_id 后,获取短期有效的 COS 下载链接。
# curl "https://api.tokentosea.com/v1/assets/ASSET_ID/download-url" \
# -H "Authorization: Bearer $TOKENTOSEA_API_KEY"# pip install requests
# export TOKENTOSEA_API_KEY="你的主站通用 API Key"
import json
import os
import time
import uuid
import requests
API_BASE = "https://api.tokentosea.com"
API_KEY = os.environ["TOKENTOSEA_API_KEY"] # 控制台「我的 Key」创建的通用 API Key
REQUEST_KEY = str(uuid.uuid4()) # Idempotency-Key:重试或查询结果时复用同一个值
AUTH = {"Authorization": f"Bearer {API_KEY}"}
PENDING = {"planned", "sending", "streaming", "upstream_pending", "billing", "archiving"}
payload = {
"model": "step-audio-2",
"messages": [{"role": "user", "content": "请简短介绍一下你能帮助我完成什么。"}],
"max_tokens": 1024,
"modalities": ["text"],
"stream": True,
}
def show(data):
print(json.dumps(data, ensure_ascii=False, indent=2))
def wait_for_result():
"""查询原请求;异步任务未结束时稍后继续查询,不要重复提交。"""
while True:
result = requests.get(f"{API_BASE}/v1/results/{REQUEST_KEY}", headers=AUTH, timeout=30).json()
if result.get("status") not in PENDING:
return result
time.sleep(5)
def events(response):
"""逐行读取 SSE,返回每个 data 帧解析后的 JSON。"""
for raw in response.iter_lines():
line = raw.decode("utf-8")
if line.startswith("data:") and line[5:].strip() not in ("", "[DONE]"):
yield json.loads(line[5:])
try:
with requests.post(
f"{API_BASE}/v1/chat/completions",
headers={**AUTH, "Idempotency-Key": REQUEST_KEY},
json=payload,
stream=True,
timeout=1300,
) as response:
print(response.status_code, response.headers.get("X-Request-Id"))
if not response.headers.get("Content-Type", "").startswith("text/event-stream"):
show(response.json()) # 错误信息或异步受理信封
else:
for event in events(response):
# 思考模型的推理过程在 delta.reasoning_content 中
for choice in event.get("choices") or []:
print((choice.get("delta") or {}).get("content") or "", end="", flush=True)
if event.get("usage"):
print("\nusage:", event["usage"])
if event.get("error"):
print("\n错误:", event["error"])
except requests.RequestException as error:
# 连接中断时请求可能已被受理:用同一个 REQUEST_KEY 查询结果,不要重新提交。
print("\n请求中断:", error)
result = wait_for_result()
print("结果:", result.get("status"), "结算:", result.get("billing_status"), result.get("error") or "")// Node.js 18+,无需额外依赖。保存为 example.mjs 后运行:node example.mjs
// export TOKENTOSEA_API_KEY="你的主站通用 API Key"
import { randomUUID } from 'node:crypto';
const API_BASE = 'https://api.tokentosea.com';
const API_KEY = process.env.TOKENTOSEA_API_KEY; // 控制台「我的 Key」创建的通用 API Key
const REQUEST_KEY = randomUUID(); // Idempotency-Key:重试或查询结果时复用同一个值
const auth = { Authorization: `Bearer ${API_KEY}` };
const PENDING = new Set(['planned', 'sending', 'streaming', 'upstream_pending', 'billing', 'archiving']);
const payload = {
model: 'step-audio-2',
messages: [{ role: 'user', content: '请简短介绍一下你能帮助我完成什么。' }],
max_tokens: 1024,
modalities: ['text'],
stream: true,
};
const show = data => console.log(JSON.stringify(data, null, 2));
// 查询原请求;异步任务未结束时稍后继续查询,不要重复提交。
async function waitForResult() {
for (;;) {
const result = await fetch(`${API_BASE}/v1/results/${REQUEST_KEY}`, { headers: auth }).then(r => r.json());
if (!PENDING.has(result.status)) return result;
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
// 逐行读取 SSE,返回每个 data 帧解析后的 JSON。
async function* events(response) {
const decoder = new TextDecoder();
let buffer = '';
for await (const chunk of response.body) {
buffer += decoder.decode(chunk, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
const data = line.startsWith('data:') ? line.slice(5).trim() : '';
if (data && data !== '[DONE]') yield JSON.parse(data);
}
}
}
try {
const response = await fetch(`${API_BASE}/v1/chat/completions`, {
method: 'POST',
headers: { ...auth, 'Idempotency-Key': REQUEST_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
console.log(response.status, response.headers.get('x-request-id'));
if (!(response.headers.get('content-type') ?? '').startsWith('text/event-stream')) {
show(await response.json()); // 错误信息或异步受理信封
} else {
for await (const event of events(response)) {
// 思考模型的推理过程在 delta.reasoning_content 中
for (const choice of event.choices ?? []) process.stdout.write(choice.delta?.content ?? '');
if (event.usage) console.log('\nusage:', event.usage);
if (event.error) console.error('\n错误:', event.error);
}
}
} catch (error) {
// 连接中断时请求可能已被受理:用同一个 REQUEST_KEY 查询结果,不要重新提交。
console.error('\n请求中断:', error.message);
}
const result = await waitForResult();
console.log('结果:', result.status, '结算:', result.billing_status, result.error ?? '');实时会话 · WebSocket#
WS /v1/realtime?model=step-audio-2 接口参考
- 需要准备单声道 PCM16 音频文件 audio.pcm(可用示例中的 ffmpeg 命令转换)
- 单次会话最多 60 秒。断线后先查询原会话状态,不会自动重连并重新生成。
// npm install ws
// export TOKENTOSEA_API_KEY="你的主站通用 API Key"
// ffmpeg -i your-audio.wav -ac 1 -ar 24000 -f s16le audio.pcm
// node realtime.mjs
import WebSocket from 'ws';
import { readFileSync } from 'node:fs';
import { randomUUID } from 'node:crypto';
const key = process.env.TOKENTOSEA_API_KEY;
const requestKey = randomUUID();
const taskId = randomUUID().replaceAll('-', '');
const kind = "session";
const init = {
"type": "session.update",
"session": {
"modalities": [
"text"
],
"instructions": "Reply briefly to the spoken greeting.",
"voice": "wenrounansheng"
}
};
const ws = new WebSocket("wss://api.tokentosea.com/v1/realtime?model=step-audio-2", {
headers: { Authorization: 'Bearer ' + key, 'Idempotency-Key': requestKey }
});
let finishing = false;
const send = value => {
if (ws.readyState !== WebSocket.OPEN || value.type === 'session.finish' && finishing) return;
if (value.type === 'session.finish') finishing = true;
ws.send(JSON.stringify({...value,...(value.type ? {event_id:'example-'+randomUUID()} : {})}));
};
let started = false;
async function produce() {
if (started) return; started = true;
if (kind === 'tts-session') {
send({type:'input_text_buffer.append', text:""});
send({type:'input_text_buffer.commit'});
send({type:'session.finish'}); return;
}
const audio = readFileSync('audio.pcm'), step = 4800;
for (let i = 0; i < audio.length && ws.readyState === WebSocket.OPEN; i += step) {
const part = audio.subarray(i, i + step);
if (kind === 'inference') ws.send(part);
else send({type:'input_audio_buffer.append', audio:part.toString('base64')});
await new Promise(resolve => setTimeout(resolve,100));
}
if (kind === 'inference') send({header:{action:'finish-task',task_id:taskId,streaming:'duplex'},payload:{input:{}}});
else send({type:kind === 'translate-auto' ? 'session.finish' : 'input_audio_buffer.commit'});
}
ws.on('open',()=>send(init));
ws.on('message',raw=>{
const event = JSON.parse(raw.toString()), type = event.type ?? event.header?.event;
console.log(event);
if (type === 'session.updated' || type === 'task-started') void produce();
if (type === 'input_audio_buffer.committed' && kind === 'session') send({type:'response.create'});
if (type === 'response.done') {
if (['tts-session','translate-commit'].includes(kind)) send({type:'session.finish'});
else if (kind !== 'translate-auto') ws.close();
}
if (['session.finished','task-finished','error','task-failed'].includes(type) || type === 'conversation.item.input_audio_transcription.completed' && kind === 'asr-session') ws.close();
});
const timer = setTimeout(()=>ws.close(),60000);
ws.on('close',async()=>{
clearTimeout(timer);
const result = await fetch('https://api.tokentosea.com/v1/results/'+requestKey,{headers:{Authorization:'Bearer '+key}}).then(r=>r.json());
console.log('Result / billing status:',result);
// 如仍在结算或处理中,继续 GET 同一地址;不要自动重新连接生成。
});
ws.on('error',error=>console.error(error.message));# pip install "websockets>=13" requests
# export TOKENTOSEA_API_KEY="你的主站通用 API Key"
# ffmpeg -i your-audio.wav -ac 1 -ar 24000 -f s16le audio.pcm
# python realtime.py
import asyncio
import base64
import json
import os
import uuid
import requests
from websockets.asyncio.client import connect
from websockets.exceptions import ConnectionClosed
API_KEY = os.environ["TOKENTOSEA_API_KEY"] # 控制台「我的 Key」创建的通用 API Key
REQUEST_KEY = str(uuid.uuid4()) # 握手 Idempotency-Key:会话结束或断线后用它查询结果
URL = "wss://api.tokentosea.com/v1/realtime?model=step-audio-2"
AUDIO_FILE = "audio.pcm" # 单声道 24000 Hz PCM16(s16le)
CHUNK = 4800 # 每 100 ms 的音频字节数
SESSION = {
"type": "session.update",
"session": {
"modalities": ["text"],
"instructions": "Reply briefly to the spoken greeting.",
"voice": "wenrounansheng",
},
}
async def run_session():
finishing = False
headers = {"Authorization": f"Bearer {API_KEY}", "Idempotency-Key": REQUEST_KEY}
async with connect(URL, additional_headers=headers) as ws:
async def send(event):
nonlocal finishing
if event.get("type") == "session.finish":
if finishing:
return
finishing = True
if "type" in event:
event = {**event, "event_id": f"example-{uuid.uuid4()}"}
try:
await ws.send(json.dumps(event, ensure_ascii=False))
except ConnectionClosed:
pass
async def produce():
with open(AUDIO_FILE, "rb") as source:
audio = source.read()
for start in range(0, len(audio), CHUNK):
await send({"type": "input_audio_buffer.append", "audio": base64.b64encode(audio[start:start + CHUNK]).decode()})
await asyncio.sleep(0.1)
await send({"type": "input_audio_buffer.commit"})
producer = None
await send(SESSION)
async for message in ws:
if isinstance(message, bytes):
print(f"<{len(message)} 字节二进制帧>")
continue
event = json.loads(message)
print(event)
name = event.get("type") or (event.get("header") or {}).get("event")
if name in ("session.updated", "task-started") and producer is None:
producer = asyncio.create_task(produce())
if name == "input_audio_buffer.committed":
await send({"type": "response.create"})
if name == "response.done":
break
if name in ("session.finished", "task-finished", "error", "task-failed"):
break
if producer:
producer.cancel()
try:
asyncio.run(asyncio.wait_for(run_session(), timeout=60)) # 单次会话最多 60 秒
except (asyncio.TimeoutError, ConnectionClosed) as error:
print("会话已结束:", type(error).__name__)
# 查询原会话的结果与结算状态;如仍在结算或处理中,继续 GET 同一地址,不要自动重连重新生成。
result = requests.get(
f"https://api.tokentosea.com/v1/results/{REQUEST_KEY}",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=30,
).json()
print("Result / billing status:", result)# 实时会话使用 WebSocket,cURL 无法收发会话事件;完整会话见 Python / Node.js 示例。
# 握手地址:wss://api.tokentosea.com/v1/realtime?model=step-audio-2
# 握手请求头:Authorization: Bearer <主站通用 API Key>;Idempotency-Key: <16–128 位幂等键>(可选)
# 首个客户端事件:session.update;音频为单声道 24000 Hz PCM16。单次会话最多 60 秒。
# 会话结束或断线后,用握手时的同一个 Idempotency-Key 查询结果与结算状态;不要自动重连重新生成。
export TOKENTOSEA_API_KEY="你的主站通用 API Key"
REQUEST_KEY="握手时发送的 Idempotency-Key"
curl "https://api.tokentosea.com/v1/results/$REQUEST_KEY" \
-H "Authorization: Bearer $TOKENTOSEA_API_KEY"相关文档#
在控制台测试#
登录后可在 模型测试 页面直接调用 step-audio-2,查看返回结果、token 用量与实际扣费。网页调用同样从账户余额扣费。