先确定是没发送,还是没收到结果
本地配置检查失败,通常还没开始请求。网络超时、断连或服务端异常,无法仅凭客户端错误判断任务有没有执行。下面是本包的保守处理规则;状态名描述客户端记录,不代替后台账单。
| 看到什么 | 当前证据 | 怎么继续 |
|---|---|---|
| 请求尚未发送未提交 | 当前证据本地校验明确失败,网络调用没有开始 | 下一步修正配置;按原业务任务重新运行 |
| 超时、断连、408 / 409 / 5xx结果未知 | 当前证据服务端是否执行、是否计费未知 | 下一步保存 unknown;核对后台记录,停止自动 POST 重提 |
| 收到 400 / 401 / 404 / 429请求被拒绝 | 当前证据已收到拒绝响应,需要核对参数、认证或额度 | 下一步保存 rejected;核对原因及账单,再人工决定下一次调用 |
| 视频返回 task ID已接受 / 运行中 | 当前证据只证明任务已经创建,不代表视频完成 | 下一步先保存 task ID;中断后继续 GET 查询同一任务 |
| 返回有效内容或 succeeded待业务验收 | 当前证据接口层完成,业务要求和实扣仍需检查 | 下一步保存结果、验收内容并核对账单 |
GET 查询可以继续,但需限制总等待时间与次数,对暂时错误采用退避和抖动、尊重 Retry-After。供应商返回未识别状态时停止自动调度。POST 创建或生成请求需要先查清原操作结果,再决定是否发起新的调用。
先核对原任务,再决定如何恢复
提交前写状态,恢复时读同一条记录
给每个业务操作一个稳定 ID,例如订单中的这次生成任务;调用者或租户也要使用稳定 scope。重试、重启、换 Key 后仍使用原 ID。输入指纹用于发现“同一个 ID 却换了任务内容”,不会把 API Key 写入记录。
- 1.
取得本地提交权
在同一个状态目录独占创建操作记录。相同调用者、相同业务 ID 的并发请求只允许一个提交者。
- 2.
先持久化 submitting
写入输入哈希、接口、时间和状态,再发一次 POST。崩溃留下 submitting 也按未知结果核对。
- 3.
保存已知结果或 unknown
保存 HTTP 状态、脱敏请求 ID、视频 task ID;超时和无效响应不会自动重提。
- 4.
恢复前检查已有记录
成功记录不再次生成;已有视频 ID 继续查询;unknown 与 rejected 都保留,人工核对后再决定新的操作。
脱敏记录示意 · 不是实际模型调用
{
"protocol": "chat",
"state": "unknown",
"reason": "timeout-network-or-invalid-response",
"httpStatus": null,
"requestId": null,
"taskId": null,
"billedUsd": null
}请求 ID 帮助对照日志,task ID 用于查询远端任务,两者不能互换。状态文件不保存 Key、提示词或响应正文;返回的原始 data 仍可能含业务内容,应用需要私下保存和脱敏。输入哈希不是加密。
用离线故障证明:未知状态不会重提
下载 ZIP 后进入 tokenba-api-kit 目录,先运行下面的现成 demo。它模拟响应丢失,并验证同 task ID 查询;不会读取真实 Key 或调用付费模型。
解压后运行 · 核心示例无需安装依赖
npm test
npm run test:python
node examples/recovery-demo.mjs
python3 examples/recovery-demo.py预期输出摘要 · 离线 mock
{
"evidence": "OFFLINE MOCK ONLY",
"actualNetworkRequests": 0,
"simulatedPosts": 1,
"firstState": "unknown",
"repeatBlocked": true
}Node:可复制的最小故障复现代码
保存为包根目录的 demo.mjs,再运行 node demo.mjs。传输函数已替换为 mock。
demo.mjs · 基于包内 src/recovery.mjs
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { submitOnce } from "./src/recovery.mjs";
const stateDir = await mkdtemp(join(tmpdir(), "recovery-demo-"));
let simulatedPosts = 0;
const context = {
stateDir, callerScope: "offline-demo", operationId: "order-001",
fetchImpl: async () => {
simulatedPosts++;
throw new Error("OFFLINE MOCK: response lost after acceptance");
},
};
try {
const input = { model: "mock-model", key: "offline-fixture-credential" };
const first = await submitOnce(input, context);
const repeat = await submitOnce(input, context);
console.log({ evidence: "OFFLINE MOCK ONLY", actualNetworkRequests: 0,
state: first.record.state, repeatBlocked: repeat.reused, simulatedPosts });
} finally { await rm(stateDir, { recursive: true, force: true }); }Python:可复制的最小故障复现代码
保存为 examples/demo.py,再运行 python3 examples/demo.py。引用同目录 request_recovery.py。
examples/demo.py · Python 标准库
from tempfile import TemporaryDirectory
from request_recovery import submit_once
posts = [0]
def lost_response(*args):
posts[0] += 1
raise TimeoutError("OFFLINE MOCK: response lost after acceptance")
with TemporaryDirectory() as state_dir:
context = dict(state_dir=state_dir, caller_scope="offline-demo",
operation_id="order-001", send=lost_response)
input_body = {"model": "mock-model"}
first = submit_once("chat", input_body, "offline-fixture-credential", **context)
repeat = submit_once("chat", input_body, "offline-fixture-credential", **context)
print({"evidence": "OFFLINE MOCK ONLY", "actualNetworkRequests": 0,
"state": first["record"]["state"], "repeatBlocked": repeat["reused"],
"simulatedPosts": posts[0]})本地验证覆盖超时、错误状态持久化、重复提交、输入碰撞、Key 轮换、进程崩溃、凭据隔离和同一视频 ID 恢复。离线 mock 不能证明真实服务端去重、费用、取消语义或生产成功率。实际运行环境为 Node 24.14.0、Python 3.14.3;最低声明版本未逐一实测。
先关掉 SDK 隐藏的重试
2026-10-07 核对官方文档:OpenAI 与 Anthropic 的 Node/TypeScript、Python SDK 对部分错误默认再重试 2 次,包括连接错误、408、409、429 和 5xx;超时也会进入重试。应用再包一层 retry,实际尝试次数可能继续增加。
测试提交恢复时,先显式关闭 SDK 自动重试,再检查应用和代理层策略。Node 的 timeout 单位为毫秒,Python 为秒;以下只展示配置,不会在网页发送请求。
Node / TypeScript · 先固定每次只有一个 SDK 尝试
const client = new OpenAI({
apiKey: process.env.tokenba_API_KEY,
baseURL: "https://api.apizt.com/v1",
maxRetries: 0,
timeout: 30_000,
});
// Anthropic 使用 API host;SDK 按 Messages 协议构造路径
const anthropic = new Anthropic({
apiKey: process.env.tokenba_API_KEY,
baseURL: "https://api.apizt.com",
maxRetries: 0,
timeout: 30_000,
});Python · 相同策略,超时单位为秒
client = OpenAI(
api_key=os.environ["tokenba_API_KEY"],
base_url="https://api.apizt.com/v1",
max_retries=0, timeout=30.0,
)
anthropic = Anthropic(
api_key=os.environ["tokenba_API_KEY"],
base_url="https://api.apizt.com",
max_retries=0, timeout=30.0,
)30 秒是这个示例的等待配置,不是模型完成时间保证。包内可选 SDK 示例没有接入持久化门禁,也未安装实测;核心故障 demo 使用原生传输模块。实际接入要锁定 SDK 版本并记录版本号,逐项验收协议。Messages 使用 2023-06-01,视频使用 /api/v3;/v1 路径不是 SDK 版本。
视频恢复只查原来的 task ID
Seedance 创建返回 ID 后,先保存,再等待。查询超时不等于视频取消;继续查询同一任务即可。创建 POST 超时却没有 ID 时,先查任务历史,不能直接换 ID 再生成。
包内视频示例 · 第一条预览;带 --run 的查询会访问 API
node examples/video-task.mjs --mode query --operation video-order-001
node examples/video-task.mjs --mode query --operation video-order-001 --run
python3 examples/video-task.py --mode query --operation video-order-001
python3 examples/video-task.py --mode query --operation video-order-001 --run先前任务须由同一种语言、相同 operation ID、原状态目录创建。每个 query 只做一次 GET,不自动循环。succeeded 后保存下载地址和 MP4;下载器单独运行,不向 CDN 发送 API Key。生成失败、过期或取消也保留原 task ID,再核对账单。
查看已有视频提交、查询与文件验收记录本地防重复的边界,要写进应用
本包约束共享同一完整本地状态目录的进程,不提供远端恰好一次保证。多机服务需要统一存储、唯一约束与访问控制;磁盘损坏、掉电、状态目录丢失和服务端超时后的执行情况仍需核对。不要删 unknown 文件或更换业务 ID 来解锁。
本包没有发送 Idempotency-Key,也没有证明 apizt 所有接口、通道与上游统一支持它。只有接口明确说明幂等范围、保存期限、参数一致性和计费语义后,才能按那份契约使用。
RFC 9110 要求:非幂等请求只有在能确认可安全重复,或原请求没有应用时,才适合自动重试。客户端超时本身不足以满足这个条件。
RFC 9110:非幂等请求的重试条件此版本未做付费 API 调用、真实超时复现或生产日志验证。服务端执行、取消、实扣、可选 SDK 的运行兼容性和最低版本矩阵仍待核对。源码按 MIT 提供,GitHub / npm 公共发布尚未完成。
把恢复流程放进真实任务
- 代码修复Chat Completions;先保存候选代码,再跑固定验收用例。
- 图片编辑multipart;保存原图和结果,超时先查记录。
- 异步视频/api/v3;同 task ID 查询,下载后播放和验收。
这些页面保留 2026-10-07 已有真实任务的参数、结果与验收范围;图片和视频实扣待核实,视频无声要求未通过。它们与本页离线恢复测试分别记录。
准备接入自己的应用
获取 API Key,选择对应协议与模型;先完成一个可验收的小任务,再把状态恢复接入自己的业务。