Chat CompletionsPOST
与 OpenAI Chat Completions 兼容的对话接口,支持流式输出、多模态输入、工具调用与各厂商的思考参数。
POST https://api.tokentosea.com/v1/chat/completions
按 OpenAI Chat Completions 协议发送对话,TokenToSea 把请求转发给型号所属厂商的官方接口,原样返回厂商的响应体(含 usage),并按实际用量从账户余额结算。
前提:一把全能密钥或模型密钥(我的 Key),账户可用余额足以覆盖本次预留。Python 示例需要 pip install openai requests,Node.js 示例需要 Node.js 18+ 与 npm install openai。
支持的型号#
截至 2026-10-21,121 个型号开放本接口:千问 61、智谱 26、阶跃星辰 11、豆包 10、MiniMax 7、Kimi 4、DeepSeek 2。当前列表以 GET /v1/models 中 verified_endpoints 包含 /v1/chat/completions 的型号为准,各型号的说明与示例见模型目录。
部分型号只开放一种响应方式:
| 型号 | 限制 |
|---|---|
qwen3-livetranslate-flash、qwen3-livetranslate-flash-2025-12-01、step-audio-2、glm-4.7-flash | 只支持流式,必须传 "stream": true |
qwen-doc-turbo | 引用上传文档(fileid://)时只支持流式;纯文本问答支持两种方式 |
请求头#
| 请求头 | 必填 | 说明 |
|---|---|---|
Authorization | 必填 | Bearer sk-relay_…,也可用 x-api-key |
Content-Type | 必填 | application/json |
Idempotency-Key | 建议 | 16–128 个字符的唯一键,见幂等 |
请求体#
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
model | string | 必填 | 型号 ID,例如 glm-5.3 |
messages | array | 必填 | 对话消息,role 为 system、user、assistant 或 tool。content 为字符串或内容块数组,见消息内容 |
max_tokens | integer | 可选 | 输出上限。部分型号使用 max_completion_tokens(见下行)。不传时按型号默认值补上(多数为 4096),不能超过该型号的 max_output_tokens |
max_completion_tokens | integer | 可选 | 与 max_tokens 含义相同,千问 3.5 及以上、Kimi K3、豆包 Seed 系列的默认输出字段。二者只传一个 |
stream | boolean | 可选 | true 时以 SSE 返回,见流式输出。型号必须已验收 SSE |
stream_options | object | 可选 | 流式时网关总会设置 include_usage: true,最后会收到一个包含 usage 的数据块 |
tools | array | 可选 | 函数工具定义。只接受 type 为 function 或 custom 的工具,其他类型(联网搜索、代码执行等托管工具)返回 422 hosted_tools_not_priced |
tool_choice | string | object | 可选 | 原样转发上游 |
service_tier | string | 可选 | 默认 standard。只有该型号价格表中列出的档位可用,否则返回 400 |
| 其他 OpenAI 兼容字段 | — | 可选 | temperature、top_p、stop、seed、response_format、frequency_penalty 等原样转发上游,是否支持以厂商文档为准 |
| 厂商扩展字段 | — | 可选 | 见厂商参数 |
请求体最大 48 MB;不支持查询字符串。
消息内容#
content 可以是字符串,也可以是内容块数组。非文本内容块只对已验收该输入类型的型号开放(见 GET /v1/models 的 verified_inputs["/v1/chat/completions"]),否则返回 422 input_modality_not_verified。
| 内容块 | 写法 | 输入类型 |
|---|---|---|
| 文本 | { "type": "text", "text": "…" } | text |
| 图片 | { "type": "image_url", "image_url": { "url": "https://… 或 data:image/png;base64,…" } } | image |
| 音频 | { "type": "input_audio", "input_audio": { "data": "…", "format": "wav" } } | audio |
| 视频 | { "type": "video_url", "video_url": { "url": "https://…" } } | video |
| 文档 | 在 system 消息中写 fileid://file_tts_…(先用 /v1/files 上传) | document,仅 qwen-doc-turbo |
图片、音频、视频 URL 必须能被厂商公网访问。单次请求最多 20 张图片、8 段视频、合计 30 个媒体输入,超出返回 422 too_many_input_media。视频会被下载以测量时长(单个文件不超过 75 MiB、时长不超过 1 小时)。
厂商参数#
以下字段由厂商定义,TokenToSea 原样转发;用 OpenAI Python SDK 时放入 extra_body。
| 参数 | 适用型号 | 说明 |
|---|---|---|
enable_thinking | 千问 | true / false 是否开启思考。qwen-max、qwen-plus 系列未传时网关按 false 发送;其余型号使用厂商默认值 |
thinking | DeepSeek、Kimi K2.6、MiniMax M2.x / M3、豆包 Seed | { "type": "enabled" } 或 { "type": "disabled" } |
reasoning_effort | Kimi K3、MiniMax M3.1、step-3.7-flash | 思考强度,取值以厂商文档为准,示例使用 "low" |
translation_options | qwen-mt-* | { "source_lang": "Chinese", "target_lang": "English" } |
modalities | 千问 Omni、阶跃音频型号 | 示例使用 ["text"] 只输出文字。请求音频输出("audio" 或 audio 字段)只对已为音频输出定价的型号开放:qwen3.5-omni-*、qwen3-omni-flash、glm-4-voice、qwen3-livetranslate-flash*,其余返回 422 audio_output_not_priced |
cache_control | 已定价显式缓存的型号 | 写在内容块上:{ "type": "ephemeral" },可选 ttl: "5m"(kimi-k3 另可 "1h")。开放型号:千问 3.5–3.8 的 Max / Plus / Flash 系列等(见型号页)、kimi-k3。千问每次最多 4 个缓存断点 |
prompt_cache_options | kimi-k3 | { "mode": "implicit", "ttl": "5m" },ttl 可为 5m 或 1h |
思考开关会影响部分型号的计价规则(例如 qwen-plus 思考与非思考输出单价不同),以型号页的价格表为准。
不支持的参数#
没有单独定价的附加产品会被拒绝,而不是静默忽略:
| 请求中出现 | 返回 |
|---|---|
web_search_options,或 enable_search / web_search 为 true | 422 hosted_tools_not_priced |
type 不是 function / custom 的工具 | 422 hosted_tools_not_priced |
mcp_servers、container、context_management、cache_id、cached_content、caching | 422 auxiliary_product_not_priced |
型号未开放显式缓存时的 cache_control;千问超过 4 个断点;不支持的 ttl | 422 explicit_cache_not_priced / cache_breakpoint_limit / cache_ttl_not_priced |
| 未定价型号上的音频输出 | 422 audio_output_not_priced |
响应#
成功时返回厂商的原生响应体,与 OpenAI chat.completion 对象一致,字段以厂商为准:
{
"id": "chatcmpl-…",
"object": "chat.completion",
"created": 1792483200,
"model": "glm-5.3",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "我可以帮你写作、翻译、总结资料和编写代码。" },
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 18,
"completion_tokens": 21,
"total_tokens": 39,
"prompt_tokens_details": { "cached_tokens": 0 }
}
}响应头 X-Request-Id 返回本次调用的幂等键。结算按 usage 中的输入、缓存命中、输出 token 计费;厂商未返回可核验的用量时,调用进入待核对(needs_review),不会按估算扣费。
如果结算尚未完成,接口返回 202 和结果信封,而不是对话内容;稍后用同一个键查询 /v1/results/{key}。使用 OpenAI SDK 时可用 with_raw_response 读取状态码。
流式输出#
"stream": true 时返回 text/event-stream,每帧为 data: {chat.completion.chunk},最后是 data: [DONE]。
- 厂商的数据块实时转发。网关总会开启
include_usage,所以会收到一个choices为空、带usage的数据块。 - 结束帧在结算完成后才发出:包含
finish_reason的数据块和[DONE]会等到主站完成扣费再一起送达。因此usage数据块可能先于finish_reason数据块到达,结束前也会多出结算所需的时间。不要假设usage是最后一帧。 - 结算失败时,流以
event: error结束:data: {"error":{"code":"execution_result_pending"}}。此时上游已经生成,用同一个键查询/v1/results/{key},不要换新键重发。OpenAI SDK 会把这一帧抛出为APIError。 - 客户端断开连接不会中断计费:网关仍会读完上游的流并结算。完整的 SSE 文本保存在结果的
output.stream中。
更多细节见流式输出指南。
示例#
基础对话#
curl https://api.tokentosea.com/v1/chat/completions \
-H "Authorization: Bearer $TOKENTOSEA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"model": "glm-5.3",
"messages": [
{"role": "system", "content": "你是一名简洁的助手。"},
{"role": "user", "content": "请简短介绍一下你能帮助我完成什么。"}
],
"max_tokens": 1024
}'import os
import uuid
from openai import OpenAI
client = OpenAI(
api_key=os.environ["TOKENTOSEA_API_KEY"],
base_url="https://api.tokentosea.com/v1",
max_retries=0,
)
request_key = str(uuid.uuid4()) # 保存它:超时或断线后用它查询结果
completion = client.chat.completions.create(
model="glm-5.3",
messages=[
{"role": "system", "content": "你是一名简洁的助手。"},
{"role": "user", "content": "请简短介绍一下你能帮助我完成什么。"},
],
max_tokens=1024,
extra_headers={"Idempotency-Key": request_key},
)
print(completion.choices[0].message.content)
print(completion.usage)import OpenAI from 'openai';
import { randomUUID } from 'node:crypto';
const client = new OpenAI({
apiKey: process.env.TOKENTOSEA_API_KEY,
baseURL: 'https://api.tokentosea.com/v1',
maxRetries: 0,
});
const requestKey = randomUUID(); // 保存它:超时或断线后用它查询结果
const completion = await client.chat.completions.create(
{
model: 'glm-5.3',
messages: [
{ role: 'system', content: '你是一名简洁的助手。' },
{ role: 'user', content: '请简短介绍一下你能帮助我完成什么。' },
],
max_tokens: 1024,
},
{ headers: { 'Idempotency-Key': requestKey } },
);
console.log(completion.choices[0].message.content);
console.log(completion.usage);流式输出#
curl -N https://api.tokentosea.com/v1/chat/completions \
-H "Authorization: Bearer $TOKENTOSEA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"model": "glm-5.3",
"messages": [{"role": "user", "content": "用三句话解释什么是向量检索。"}],
"max_tokens": 1024,
"stream": true
}'import os
import uuid
import openai
from openai import OpenAI
client = OpenAI(
api_key=os.environ["TOKENTOSEA_API_KEY"],
base_url="https://api.tokentosea.com/v1",
max_retries=0,
)
request_key = str(uuid.uuid4())
usage = None
try:
stream = client.chat.completions.create(
model="glm-5.3",
messages=[{"role": "user", "content": "用三句话解释什么是向量检索。"}],
max_tokens=1024,
stream=True,
extra_headers={"Idempotency-Key": request_key},
)
for chunk in stream:
if chunk.usage: # 用量块的 choices 为空,且可能早于结束块到达
usage = chunk.usage
for choice in chunk.choices:
if choice.delta.content:
print(choice.delta.content, end="", flush=True)
print("\n", usage)
except openai.APIError as error:
# 流中途的 execution_result_pending 等错误:用同一个键查询结果,不要换新键重发
print("调用未正常结束,请查询 /v1/results/" + request_key, error)import OpenAI from 'openai';
import { randomUUID } from 'node:crypto';
const client = new OpenAI({
apiKey: process.env.TOKENTOSEA_API_KEY,
baseURL: 'https://api.tokentosea.com/v1',
maxRetries: 0,
});
const requestKey = randomUUID();
let usage = null;
try {
const stream = await client.chat.completions.create(
{
model: 'glm-5.3',
messages: [{ role: 'user', content: '用三句话解释什么是向量检索。' }],
max_tokens: 1024,
stream: true,
},
{ headers: { 'Idempotency-Key': requestKey } },
);
for await (const chunk of stream) {
if (chunk.usage) usage = chunk.usage; // choices 为空的用量块
for (const choice of chunk.choices) process.stdout.write(choice.delta?.content ?? '');
}
console.log('\n', usage);
} catch (error) {
console.error(`调用未正常结束,请查询 /v1/results/${requestKey}`, error.message);
}图片输入#
qwen3.8-max 在本接口已验收文本与图片输入。千问型号用 enable_thinking 控制是否思考。
curl https://api.tokentosea.com/v1/chat/completions \
-H "Authorization: Bearer $TOKENTOSEA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"model": "qwen3.8-max",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "这张图片里有什么?"},
{"type": "image_url", "image_url": {"url": "https://example.com/photo.jpg"}}
]
}],
"max_tokens": 1024,
"enable_thinking": false
}'import os
import uuid
from openai import OpenAI
client = OpenAI(
api_key=os.environ["TOKENTOSEA_API_KEY"],
base_url="https://api.tokentosea.com/v1",
max_retries=0,
)
IMAGE_URL = "https://example.com/photo.jpg" # 厂商需能公网访问
completion = client.chat.completions.create(
model="qwen3.8-max",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "这张图片里有什么?"},
{"type": "image_url", "image_url": {"url": IMAGE_URL}},
],
}],
max_tokens=1024,
extra_body={"enable_thinking": False}, # 厂商扩展参数放在 extra_body
extra_headers={"Idempotency-Key": str(uuid.uuid4())},
)
print(completion.choices[0].message.content)import OpenAI from 'openai';
import { randomUUID } from 'node:crypto';
const client = new OpenAI({
apiKey: process.env.TOKENTOSEA_API_KEY,
baseURL: 'https://api.tokentosea.com/v1',
maxRetries: 0,
});
const IMAGE_URL = 'https://example.com/photo.jpg'; // 厂商需能公网访问
const completion = await client.chat.completions.create(
{
model: 'qwen3.8-max',
messages: [{
role: 'user',
content: [
{ type: 'text', text: '这张图片里有什么?' },
{ type: 'image_url', image_url: { url: IMAGE_URL } },
],
}],
max_tokens: 1024,
enable_thinking: false, // 厂商扩展参数直接写在请求对象中
},
{ headers: { 'Idempotency-Key': randomUUID() } },
);
console.log(completion.choices[0].message.content);工具调用#
函数工具由厂商型号执行,TokenToSea 只转发定义并按 token 计费。是否支持、是否支持并行调用以厂商为准,正式使用前可在控制台 → 模型测试验证。
curl https://api.tokentosea.com/v1/chat/completions \
-H "Authorization: Bearer $TOKENTOSEA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"model": "glm-5.3",
"messages": [{"role": "user", "content": "上海今天天气怎么样?"}],
"max_tokens": 1024,
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询城市的实时天气",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string", "description": "城市名"}},
"required": ["city"]
}
}
}]
}'import json
import os
import uuid
from openai import OpenAI
client = OpenAI(
api_key=os.environ["TOKENTOSEA_API_KEY"],
base_url="https://api.tokentosea.com/v1",
max_retries=0,
)
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询城市的实时天气",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string", "description": "城市名"}},
"required": ["city"],
},
},
}]
messages = [{"role": "user", "content": "上海今天天气怎么样?"}]
first = client.chat.completions.create(
model="glm-5.3", messages=messages, tools=tools, max_tokens=1024,
extra_headers={"Idempotency-Key": str(uuid.uuid4())},
)
message = first.choices[0].message
if message.tool_calls:
messages.append(message.model_dump(exclude_none=True))
for call in message.tool_calls:
args = json.loads(call.function.arguments)
result = {"city": args["city"], "weather": "晴", "temperature_c": 22} # 调用你自己的服务
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result, ensure_ascii=False)})
final = client.chat.completions.create(
model="glm-5.3", messages=messages, tools=tools, max_tokens=1024,
extra_headers={"Idempotency-Key": str(uuid.uuid4())}, # 新的一轮使用新键
)
print(final.choices[0].message.content)import OpenAI from 'openai';
import { randomUUID } from 'node:crypto';
const client = new OpenAI({
apiKey: process.env.TOKENTOSEA_API_KEY,
baseURL: 'https://api.tokentosea.com/v1',
maxRetries: 0,
});
const tools = [{
type: 'function',
function: {
name: 'get_weather',
description: '查询城市的实时天气',
parameters: {
type: 'object',
properties: { city: { type: 'string', description: '城市名' } },
required: ['city'],
},
},
}];
const messages = [{ role: 'user', content: '上海今天天气怎么样?' }];
const first = await client.chat.completions.create(
{ model: 'glm-5.3', messages, tools, max_tokens: 1024 },
{ headers: { 'Idempotency-Key': randomUUID() } },
);
const message = first.choices[0].message;
if (message.tool_calls?.length) {
messages.push(message);
for (const call of message.tool_calls) {
const args = JSON.parse(call.function.arguments);
const result = { city: args.city, weather: '晴', temperature_c: 22 }; // 调用你自己的服务
messages.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(result) });
}
const final = await client.chat.completions.create(
{ model: 'glm-5.3', messages, tools, max_tokens: 1024 },
{ headers: { 'Idempotency-Key': randomUUID() } }, // 新的一轮使用新键
);
console.log(final.choices[0].message.content);
}超时后恢复结果#
请求超时或连接中断时,不确定调用是否已经执行。用原来的键查询结果;返回 404 result_not_found 且原请求确实没有得到响应时,用同一个键、同一个请求体重发是安全的:已受理的调用只会返回原结果。
curl "https://api.tokentosea.com/v1/results/$REQUEST_KEY" \
-H "Authorization: Bearer $TOKENTOSEA_API_KEY"import os
import time
import requests
def wait_for_result(request_key: str, timeout_s: int = 300) -> dict:
url = "https://api.tokentosea.com/v1/results/" + request_key
headers = {"Authorization": "Bearer " + os.environ["TOKENTOSEA_API_KEY"]}
deadline = time.time() + timeout_s
while time.time() < deadline:
response = requests.get(url, headers=headers, timeout=30)
if response.status_code == 404:
raise LookupError("该键没有已受理的调用,可用同一个键与请求体重发")
response.raise_for_status()
result = response.json()
if result["status"] in ("succeeded", "failed"):
return result
time.sleep(5)
raise TimeoutError("结果仍在处理,稍后用同一个键继续查询")async function waitForResult(requestKey, timeoutMs = 300_000) {
const url = `https://api.tokentosea.com/v1/results/${requestKey}`;
const headers = { Authorization: `Bearer ${process.env.TOKENTOSEA_API_KEY}` };
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const response = await fetch(url, { headers });
if (response.status === 404) throw new Error('该键没有已受理的调用,可用同一个键与请求体重发');
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
const result = await response.json();
if (['succeeded', 'failed'].includes(result.status)) return result;
await new Promise((resolve) => setTimeout(resolve, 5000));
}
throw new Error('结果仍在处理,稍后用同一个键继续查询');
}成功结果的 output 是原响应体;流式调用的 output.stream 是完整的 SSE 文本。
错误#
本接口特有的错误如下,通用错误见 API 概览。
| 状态码 | code | 原因与处理 |
|---|---|---|
400 | main_wallet_authorization_failed | 输出上限超过 max_output_tokens,或输入长度超过价格表上限。调小 max_tokens 或缩短输入 |
409 | main_wallet_authorization_failed | 余额不足、同时处理中的调用超过 8 个,或同一个键对应了不同请求体 |
422 | response_mode_not_verified | 型号未开放所选响应方式(例如只支持流式的型号未传 stream: true) |
422 | streaming_required | 该型号要求 "stream": true |
422 | input_modality_not_verified | 型号未开放图片 / 音频 / 视频 / 文档输入 |
422 | too_many_input_media、input_media_unavailable、media_duration_invalid | 媒体数量超限、URL 无法访问或视频时长无效 |
422 | hosted_tools_not_priced、auxiliary_product_not_priced、explicit_cache_not_priced、audio_output_not_priced | 见不支持的参数 |
4xx | provider_http_<状态码> | 厂商拒绝了请求,message 中是脱敏后的厂商描述。未扣费 |
502 | provider_http_5xx、execution_result_pending | 厂商失败或结果待确认。用同一个键查询结果 |