TokenToSea开发者文档
控制台

幂等与重试

用 Idempotency-Key 保证同一个请求只生成和计费一次,并在网络中断、超时或结果未知时安全地取回结果。

本页内容

模型调用会产生费用,而且生成结果每次都不同。网络在请求途中中断时,你无法知道平台是否已经收到请求。Idempotency-Key 让你可以放心地“再问一次”:同一个键在同一把密钥下只会执行一次,之后可以随时用它取回结果。

前提:已创建密钥并把它设置为环境变量 TOKENTOSEA_API_KEY。示例使用 Python requests 或 Node.js 18+ 内置 fetch。

工作方式#

  • 在每个 POST 请求上带 Idempotency-Key 请求头。
  • 平台按「密钥 + 幂等键」识别请求。同一把密钥下,一个键只对应一次调用;不同密钥的键互不影响。
  • 响应头 X-Request-Id 与 X-TokenToSea-Request-Id 回显这个键。
  • 不带这个请求头时,平台自动生成 tts-auto-<UUID> 并通过上述响应头返回。但如果连接在响应头到达前就断开,你将无法得知这个键,所以生产环境请始终自己生成并保存。

格式#

规则值
长度16–128 个字符
允许的字符字母、数字、_、.、:、-
推荐每个业务操作一个 UUID v4,例如 3f0c2a1e-8b7d-4c55-9a6e-2d1f0b9e7c44
不合规时400 idempotency_key_invalid

不要在幂等键中放入密钥、手机号、邮箱等敏感信息:它会出现在响应头和你的日志中。

重复请求时会发生什么#

场景平台行为
同一个键、相同的请求,原调用已完成不重新生成、不重复计费,直接返回原结果。原调用是流式的,会一次性返回完整的 SSE 文本;原调用失败的,返回 422 与结果信封(见下文)。
同一个键、相同的请求,原调用仍在处理返回 202 与结果信封,按其中的 status 继续查询。
同一个键、相同的型号、不同的请求体409(错误码 main_wallet_authorization_failed)。原调用不受影响。
同一个键、不同的 model502 execution_result_pending:这个键已绑定原调用的型号。原调用不受影响;此时用这个键查询到的是原调用的结果,不是这次请求的。
同一个键,但找不到可返回的结果409 request_already_accepted。用 GET /v1/results/{键} 查询原调用状态。
不同的键视为全新的调用,会再次生成并计费。

幂等键会被永久占用

一个键一旦被受理(产生了调用记录),就不能再用于另一个请求,即使原调用失败或结果已过期。修改请求后重新发起时,请生成新键。

什么时候重发,什么时候查询#

判断依据只有一个:平台是否已经给出明确结果。

你收到的结果是否明确正确做法
200 和响应体明确:成功使用结果
4xx 和错误体(下面两种除外)明确:这次没有执行,也没有计费按错误码修正请求或等待条件满足,然后用新键发起
422 和结果信封(含 run_id,status 为 failed)明确:原调用已失败按 billing_status 判断是否扣费,见下文
409 request_already_accepted / request_already_dispatched原调用已受理用原键查询
202未完成用原键查询,直到终态
任何 5xx(包括 502 和 503),或流式响应以 event: error 结束未知用原键查询,不要换键重发
网络错误、连接重置、客户端超时未知用原键查询 /v1/results/{键}

503 同样可能出现在厂商已经返回结果之后:例如结算服务暂时不可用时,平台会保存厂商结果、稍后在后台完成结算,但这次请求返回 503。换新键重发一个结果未知的请求,可能导致同一任务被生成并计费两次。

失败的结果信封#

422 响应体如果是结果信封(含 run_id),说明同一个键的原调用已经结束且失败,不是本次请求被拒绝:

billing_status含义做法
failed未扣费,预留已释放修正后用新键发起
succeeded已扣费,但结果无法交付(error.code 为 paid_result_unavailable)不要重发,带上 run_id 与幂等键联系支持

取回结果#

HTTP
GET /v1/results/{Idempotency-Key}
Authorization: Bearer <创建该请求的同一把密钥>

返回结果信封,status 为 succeeded 时 output 中是原始响应。查询规则:

  • 必须使用发起请求的同一把密钥;换一把密钥查询返回 404 result_not_found。
  • 结果在最后一次更新后保留约 31 天。
  • 平台在把请求发往厂商之前就会建立结果记录。因此查询返回 404 result_not_found 时,这个键对应的请求一定没有发往厂商:可能从未到达平台,也可能在受理前被拒绝。此时用原键原样重发是安全的;如果原请求其实正在受理,平台会识别出同一个键,不会重复执行。
  • 原样重发后收到 409 request_already_accepted,而查询持续 1 分钟以上仍返回 404:这个键已被占用,但请求从未发往厂商(例如受理后因价格更新被撤回)。此时改用新键发起。
  • status 停留在 planned 超过 5 分钟:请求没有发往厂商,不会生成也不会计费,同样改用新键发起。

完整示例:可安全重试的调用#

下面的函数在结果未知时不重新生成,而是改为按幂等键查询;只有确认请求没有发往厂商时,才重发:平台没有记录时用原键,键已被占用时用新键。

import os
import time
import uuid

import requests

API = "https://api.tokentosea.com/v1"
AUTH = {"Authorization": f"Bearer {os.environ['TOKENTOSEA_API_KEY']}"}
FINAL = {"succeeded", "failed", "needs_review"}
ACCEPTED = {"request_already_accepted", "request_already_dispatched"}


def fetch_result(key, deadline, grace=0):
    """按幂等键查询直到终态,或 planned 超过 5 分钟;持续 grace 秒没有记录时返回 None。"""
    missing_since = planned_since = None
    while time.time() < deadline:
        try:
            r = requests.get(f"{API}/results/{key}", headers=AUTH, timeout=30)
        except requests.RequestException:
            r = None  # 查询本身失败:稍后再查
        if r is not None and r.status_code == 404:
            missing_since = missing_since or time.time()
            if time.time() - missing_since >= grace:
                return None
        elif r is not None and r.ok:
            envelope = r.json()
            if envelope["status"] in FINAL:
                return envelope
            if envelope["status"] != "planned":
                planned_since = None
            elif planned_since is None:
                planned_since = time.time()
            elif time.time() - planned_since > 300:
                return envelope  # 没有发往厂商
        time.sleep(5)
    raise TimeoutError(f"结果仍在处理,稍后用同一幂等键查询:{key}")


def call(path, body, attempts=3):
    key = str(uuid.uuid4())  # 生产环境请先把 key 与业务记录一起持久化
    deadline = time.time() + 30 * 60
    for _ in range(attempts):
        code = None
        try:
            r = requests.post(f"{API}{path}", json=body, headers={**AUTH, "Idempotency-Key": key}, timeout=(10, 1300))
        except requests.RequestException:
            r = None  # 网络错误或超时:结果未知
        if r is not None:
            if r.status_code == 200:
                return {"status": "succeeded", "output": r.json()}
            data = r.json() if "json" in r.headers.get("content-type", "") else {}
            if r.status_code == 422 and "run_id" in data:
                return data  # 原调用已失败;billing_status 为 succeeded 时已扣费
            code = data.get("error", {}).get("code")
            if r.status_code < 500 and r.status_code != 202 and code not in ACCEPTED:
                r.raise_for_status()  # 明确拒绝、没有执行:修正请求后用新键发起
        # 202、5xx、原调用已受理、网络错误:结果未知,先按原键查询
        envelope = fetch_result(key, deadline, grace=60 if code in ACCEPTED else 0)
        if envelope is not None and envelope["status"] != "planned":
            return envelope
        if envelope is not None or code in ACCEPTED:
            key = str(uuid.uuid4())  # 键已被占用,但请求从未发往厂商:换新键
        # 否则平台没有这个键的记录:下一轮用原键原样重发
    raise RuntimeError(f"多次尝试仍无结果,幂等键:{key}")


result = call("/chat/completions", {
    "model": "qwen3.8-flash",
    "messages": [{"role": "user", "content": "你好"}],
    "max_tokens": 128,
})
print(result["status"], result.get("output"))

注意:示例针对返回 JSON 的接口。语音合成成功时返回音频二进制,流式请求成功时返回 SSE,读取方式见语音合成与流式输出。

特殊情况#

接口说明
WS /v1/realtime在握手请求头中带 Idempotency-Key。同一个键的第二次连接会被拒绝。断线后查询 /v1/results/{键},不要用同一个键重连。
POST /v1/files、POST /v1/audio/voices同样按幂等键去重:同键同内容返回原文件或音色,同键不同内容返回 409 idempotency_conflict。这两个接口不产生 /v1/results 记录。
异步向量(text-embedding-async-*)同键不同内容返回 409 idempotency_conflict。
工具 API使用请求体中的 requestKey 字段实现同样的保证,见工具 API。

SDK 中的用法#

OpenAI 与 Anthropic SDK 都不会自动发送 Idempotency-Key。请通过 extra_headers(Python)或请求选项 headers(Node.js)传入,并关闭 SDK 自动重试:SDK 默认会在网络错误、408、409、429 和 5xx 时自动重发,不带幂等键时每次重发都是一次新的调用。SDK 还会把 202 当作成功响应解析,检测方法见用官方 SDK 接入。

下一步#

    输入关键词搜索全部文档与模型。