Permalink to 异步任务异步任务

图片、视频和音乐都是异步的。创建端点不会等产物:它校验请求、预扣额度、写入一条持久化的 Job,然后立刻返回 202 Accepted。之后由你轮询任务状态,成功后再去要带签名的下载链接。

这是整套 API 里最容易让接入方意外的一块,动手写代码之前值得先读完。

text
POST /v1/images/generations        ─┐
POST /v1/images/edits               │
POST /v1/videos                     ├─► 202 { "id": "job_…", "status": "queued" }
POST /v1/audio/music               ─┘

                     GET /v1/jobs/{job_id}          ← 轮询到终态

                     GET /v1/jobs/{job_id}/assets   ← 签名 URL,15 分钟有效

Chat completions、语音合成和转写不是 Job,结果直接在响应体里返回。只有上面这三类生成式媒体走 这条链路。

Permalink to job-信封Job 信封

每个创建端点和每次状态查询返回的都是同一个对象:

json
{
  "id": "job_2f81c0a4d3b57e9016ab24cf",
  "status": "queued",
  "kind": "video",
  "model": "bytedance/seedance-2",
  "progress": 0,
  "created_at": 1787824589,
  "updated_at": 1787824589,
  "finished_at": 0,
  "dispatch_deadline_at": 1787825189,
  "error_code": "",
  "error": ""
}
字段说明
id不透明任务 id,前缀 job_。你只需要拿住这一个句柄
status见下面的状态表
kindimagevideoaudio
model你请求的模型名,不是我们实际路由到的上游模型名
progress0100。仅供参考:很多上游只会报 0 然后 100
created_atUnix 秒
updated_at未设置时省略
finished_atUnix 秒,进入终态后出现
dispatch_deadline_at任务必须在此之前被投递到上游
error_codeerror非成功终态时出现,其余情况省略

Permalink to 状态机状态机

text
queued ──► running ──► succeeded
   │           └─────► failed
   └─► queue_expired
状态终态含义
queued已受理、额度已预扣,正在等上游产能
running上游已接收,正在生成
succeeded产物已落盘,可以下载
failed生成失败,或上游成功但没有任何我们能交付的产物
queue_expireddispatch_deadline_at 之前始终没能投递到上游

canceled 只为解码历史数据保留,新任务不会进入这个状态。

没有取消接口

Job 一旦创建就会跑到终态。关掉客户端、断开连接或停止轮询只会让你不再观察它,既不会停止任务, 也不会因此退款。你在旧资料里看到的任何 cancel 形状的 URL,都只会返回一个普通的 404

Permalink to 投递期限投递期限

dispatch_deadline_at 是创建后十分钟。它只约束排队这一段:我们必须在这段时间内找到有空闲 产能的上游并把任务交出去。生成本身不受它约束——第 599 秒才进入上游也没问题,长视频在 running 状态下跑过这个时间点是常态。

如果期限到了任务还停在 queued,它会以 queue_expired 终结,并且预扣的额度全额返还。这是从 外部唯一能看到的超时。

Permalink to 轮询轮询

面向客户没有 webhook,请轮询 GET /v1/jobs/{job_id}

bash
curl https://hypit.ai/v1/jobs/job_2f81c0a4d3b57e9016ab24cf \
  -H "Authorization: Bearer $HYPIT_API_KEY"

合理的节奏:首次约 5 秒后查,之后每 3–10 秒一次,视频再拉长退避。图片通常几十秒出结果,视频以 分钟计。

python
import os, time, httpx

BASE = os.environ.get("HYPIT_BASE_URL", "https://hypit.ai/v1")
AUTH = {"Authorization": f"Bearer {os.environ['HYPIT_API_KEY']}"}
TERMINAL = {"succeeded", "failed", "queue_expired"}

def wait_for(job_id: str, timeout: float = 900.0) -> dict:
    deadline = time.monotonic() + timeout
    delay = 3.0
    while True:
        r = httpx.get(f"{BASE}/jobs/{job_id}", headers=AUTH, timeout=30)
        r.raise_for_status()
        job = r.json()
        if job["status"] in TERMINAL:
            return job
        if time.monotonic() > deadline:
            raise TimeoutError(f"{job_id} 仍处于 {job['status']}")
        time.sleep(delay)
        delay = min(delay * 1.4, 15.0)
js
const BASE = process.env.HYPIT_BASE_URL ?? "https://hypit.ai/v1";
const AUTH = { Authorization: `Bearer ${process.env.HYPIT_API_KEY}` };
const TERMINAL = new Set(["succeeded", "failed", "queue_expired"]);

export async function waitFor(jobId, timeoutMs = 900_000) {
  const deadline = Date.now() + timeoutMs;
  let delay = 3_000;
  for (;;) {
    const r = await fetch(`${BASE}/jobs/${jobId}`, { headers: AUTH });
    if (!r.ok) throw new Error(`${r.status} ${await r.text()}`);
    const job = await r.json();
    if (TERMINAL.has(job.status)) return job;
    if (Date.now() > deadline) throw new Error(`${jobId} 仍处于 ${job.status}`);
    await new Promise((res) => setTimeout(res, delay));
    delay = Math.min(delay * 1.4, 15_000);
  }
}

Permalink to 取产物取产物

succeeded 的任务从 assets 路由取产物。产物一律不公开:我们按需为你在当次请求里对确切的存储 对象签一个短期 URL。

bash
curl https://hypit.ai/v1/jobs/job_2f81c0a4d3b57e9016ab24cf/assets \
  -H "Authorization: Bearer $HYPIT_API_KEY"
json
{
  "items": [
    {
      "id": "ast_9c3d1e7a44b0",
      "job_id": "job_2f81c0a4d3b57e9016ab24cf",
      "ordinal": 0,
      "kind": "video",
      "storage": "s3",
      "mime_type": "video/mp4",
      "bytes": 4718592,
      "width": 1920,
      "height": 1080,
      "seconds": 6,
      "url": "https://…?X-Amz-Signature=…",
      "expires_at": 1787825489,
      "created_at": 1787825189
    }
  ]
}
字段说明
id稳定的产物 id
ordinal该任务内的序号;图片任务 n: 3 会得到序号 0、1、2
kindimagevideoaudiothumbnail
storage恒为 s3
widthheightseconds该媒介已知时出现
url现签的链接,有效期 15 分钟
expires_aturl 失效的 Unix 秒

响应带 Cache-Control: private, no-store不要持久化 url,请持久化 job_id,需要时重新调用 本路由重签。签名从存储的 bucket 与 object key 现场生成,只要产物还在保留期内,重签总是成功。

成功产物从任务进入终态起保留 30 天。需要长期保存的请自行下载或转存。

python
job = wait_for(job_id)
if job["status"] != "succeeded":
    raise RuntimeError(f"{job['status']}: {job.get('error_code')} {job.get('error')}")

assets = httpx.get(f"{BASE}/jobs/{job_id}/assets", headers=AUTH, timeout=30).json()["items"]
for a in assets:
    with httpx.stream("GET", a["url"], timeout=None, follow_redirects=True) as src:
        src.raise_for_status()
        with open(f"out-{a['ordinal']}.{a['mime_type'].split('/')[-1]}", "wb") as f:
            for chunk in src.iter_bytes():
                f.write(chunk)

Permalink to 模态别名模态别名

为了可读性,有三对别名指向完全相同的处理逻辑:

别名等价于
GET /v1/videos/{job_id}GET /v1/jobs/{job_id}
GET /v1/audio/music/{job_id}GET /v1/jobs/{job_id}
GET /v1/audio/music/{job_id}/content同上

音频 /content 是给 curl -L 用的便利路由。视频下载统一使用按归属校验的 assets 接口, 返回新的签名 URL;任务还没有可交付产物时返回 409 job_asset_not_ready

bash
curl https://hypit.ai/v1/jobs/job_2f81c0a4d3b57e9016ab24cf/assets \
  -H "Authorization: Bearer $HYPIT_API_KEY"

Permalink to 幂等幂等

在任意创建请求上带 Idempotency-Key,重试就变安全了:

bash
curl https://hypit.ai/v1/videos \
  -H "Authorization: Bearer $HYPIT_API_KEY" \
  -H "Idempotency-Key: order-4471-clip-1" \
  -H "Content-Type: application/json" \
  -d '{"model": "bytedance/seedance-2", "prompt": "水沟里漂着一只纸船", "seconds": 5}'

这个键的作用域是你的账号加当前凭据,最长 512 字节。

情况结果
同键、同请求返回原来那个 Job,不会重复计费
同键、不同请求409 idempotency_conflict
同键、上一次仍在准备中425 admission_in_progress,稍后重试
不带键即使请求体完全相同,每次调用也都创建新任务

是否重放由请求的持久化指纹判定,因此某个模型后来下线,也不会改变你用同一个键拿回的结果。

Permalink to 并发上限并发上限

一个账号同时最多持有 64queuedrunning 的任务,跨所有 API Key、OAuth 授权和控制台 会话合并计算。超过就是 429

json
{
  "error": {
    "message": "too many queued or running media Jobs for this account; retry after one finishes",
    "type": "rate_limit_error",
    "code": "media_open_job_capacity",
    "param": null
  }
}

响应带 Retry-After: 30。这条限制与错误与限流里那条按 Key 的请求速率 限制是两回事。

Permalink to 额度怎么走额度怎么走

提交任务会按估算预扣额度。进入终态时:

  • succeeded——按真实用量结算。低于估算的部分退回,高于估算的部分补扣。
  • failed——已经实际扣掉的全额退回,退到原来那几笔额度上;任务的费用列归零。
  • queue_expired——同样全额退回。

这也覆盖了那个尴尬的情况:上游确实生成了东西,但我们拿不到可交付的产物时,任务判为失败并退款, 你不会为一个下载不到的产物付钱。机制细节见额度与计费

Permalink to 查询类错误查询类错误

状态code含义
404job_not_foundid 不存在——或者这个任务属于别的账号。两者刻意不可区分
409job_asset_not_ready/content:还没有可交付的产物
503job_service_unavailable存储或签名短暂不可用,请重试