Python
SDK
HiAPI 官方 SDK 帮你走完生成任务的整个流程:提交任务、轮询进度,最后返回一个任务对象,里面带着产出内容的 URL。幂等重试(可选开启)、类型化错误、webhook 验签这些都已经内置好了,不用自己再写轮询、退避重试或响应解析的逻辑。
Go
Go 1.21+。纯标准库客户端;每个任务 API 方法都接受 context.Context,支持多个 goroutine 并发调用。
go get github.com/HiAPIAI/hiapi-go · v0.2.1 · pkg.go.dev · GitHub
Java
Java 11+。仅用 JDK 自带能力(java.net.http.HttpClient)的客户端,响应对象不可变,无需管理任何运行时依赖。
ai.hiapi:hiapi · v0.2.1 · Maven Central · GitHub
Node.js
没有你需要的语言?用任意 HTTP 客户端直接调统一异步接口即可——这正是每个 SDK 底层封装的内容。
每个 SDK 都自带这些能力
Section titled “每个 SDK 都自带这些能力”- 一次调用,从提交到完成。
run提交任务并替你轮询——想自己接管每一步,就直接用create/retrieve/list/wait(Java 里叫waitFor)。 - 幂等重试(可选开启)。 传入幂等键后,连接中断的重试就变得安全,不会有重复创建(和重复计费)任务的风险;不传就是默认关闭,由你决定何时启用。
- 模型线路,无需手动拼接字符串。 遇到提供多个处理选项的模型时,用一个普通参数选择,不用手动拼接
model@route名称。 - 类型化错误。 认证失败、模型不可用、超时、幂等键冲突这些常见失败,在各语言里都映射为独立可捕获的错误类型,常见情况不用再解析通用 HTTP 异常。
- Webhook 验签辅助。 一个方法替你校验回调的签名与时间戳——不用自己写 HMAC 比对逻辑。
开始前:先按上面的方式装好 SDK,然后去 HiAPI 账户拿一个 API Key 并确保有余额——生成请求会扣费,先去对应的模型页看一眼价格。
提交任务、跟踪进度、读取结果:
from hiapi import HiAPI
# 这里直接写 api_key 是为了示例清晰;生产环境建议从环境变量# (HIAPI_API_KEY)读取,不要硬编码。client = HiAPI(api_key="sk-...")
task = client.tasks.run( model="happyhorse-1-0", input={"prompt": "a cyan glass data center entrance", "duration": 5, "resolution": "720p"}, on_update=lambda t: print("status:", t.status),)
for out in task.output: print(out.type, out.url)package main
import ( "context" "fmt" "log"
hiapi "github.com/HiAPIAI/hiapi-go")
func main() { // 这里直接写 apiKey 是为了示例清晰;生产环境建议从环境变量 // (HIAPI_API_KEY)读取,不要硬编码。 client, err := hiapi.New("sk-...") if err != nil { log.Fatal(err) }
task, err := client.Tasks.Run(context.Background(), hiapi.RunParams{ Model: "happyhorse-1-0", Input: map[string]any{"prompt": "a cyan glass data center entrance", "duration": 5, "resolution": "720p"}, OnUpdate: func(t *hiapi.Task) { log.Println("status:", t.Status) }, }) if err != nil { log.Fatal(err) }
for _, out := range task.Output { fmt.Println(out.Type, out.URL) }}import ai.hiapi.HiAPI;import ai.hiapi.Task;import ai.hiapi.Output;import ai.hiapi.RunOptions;import java.util.Map;
public class Quickstart { public static void main(String[] args) { // 这里直接写 apiKey 是为了示例清晰;生产环境建议从环境变量 // (HIAPI_API_KEY)读取,不要硬编码。 HiAPI client = new HiAPI("sk-...");
Task task = client.tasks().run( "happyhorse-1-0", Map.of( "prompt", "a cyan glass data center entrance", "duration", 5, "resolution", "720p" ), RunOptions.builder() .onUpdate(t -> System.out.println("status: " + t.getStatus())) .build() );
for (Output out : task.getOutput()) { System.out.println(out.getType() + " " + out.getUrl()); } }}import { HiAPI } from "hiapi_ai";// ESM 写法请保存为 .mjs,或在 package.json 中设置 "type": "module"。// CommonJS 请把 import 换成下面这行,并保存为 .cjs:// const { HiAPI } = require("hiapi_ai");
async function main() { // 这里直接写 apiKey 是为了示例清晰;生产环境建议从环境变量 // (HIAPI_API_KEY)读取,不要硬编码。 const client = new HiAPI({ apiKey: "sk-..." });
const task = await client.tasks.run({ model: "happyhorse-1-0", input: { prompt: "a cyan glass data center entrance", duration: 5, resolution: "720p" }, onUpdate: (t) => console.log("status:", t.status), });
for (const out of task.output) { console.log(out.type, out.url); }}
main().catch((err) => { console.error(err); process.exitCode = 1;});run 会一直轮询,直到任务成功、失败,或客户端超时(默认 10 分钟)。超时只是让 SDK 停止轮询——不会取消任务,任务仍可能在后台跑完并产生费用。遇到超时,用任务 id 调 retrieve 去查结果,不要重新提交同一个请求。若需要生命周期的完全控制,直接用 create / retrieve / list / wait(Java 里叫 waitFor)——详见各 SDK 的 README:Python · Go · Java · Node.js。
上面拿到的 output URL 是临时的——HiAPI 大约在创建后 7 天自动删除。Node.js SDK 可以在创建任务时传 storage: "persistent" 直接指定长期存储(按大小计费;余额不足会静默降级为 "temp",实际使用的档位以 task.storage 为准);Python、Go、Java 三个 SDK 暂未提供创建时指定的方式,想在到期前留住某个产物,需要转为持久存储(也可以在控制台里操作)。存储档位与计费详见产物存储。
到这里已经跑通了一次调用。下面几节讲的是正式上线后你会用到的东西:选择模型线路、让重试变得安全、验签 webhook 回调,以及处理错误。
部分模型页会列出不止一个处理选项——比如 ext——对应不同价格或可用性。把那个页面上显示的线路值原样传进参数,不用手动拼接 model@route 名称:
created = client.tasks.create( model="gpt-image-2/text-to-image", route="ext", # 优先于旧的 "model@ext" 后缀写法 input={"prompt": "..."},)created, err := client.Tasks.Create(context.Background(), hiapi.CreateParams{ Model: "gpt-image-2/text-to-image", Route: "ext", // 优先于 Model: "...@ext" Input: map[string]any{"prompt": "..."},})CreatedTask created = client.tasks().create( "gpt-image-2/text-to-image", Map.of("prompt", "..."), CreateOptions.builder().route("ext").build() // 优先于 "...@ext");const created = await client.tasks.create({ model: "gpt-image-2/text-to-image", route: "ext", // 优先于把线路写进 model 字符串 input: { prompt: "..." },});省略线路参数(或传 "default")会使用模型的默认线路。未知线路会在任务创建、计费之前就被拒绝,返回 400 并列出可用线路。旧代码里的 x@ext 后缀写法仍然可用。
设置幂等键(作为 Idempotency-Key 请求头发送,≤255 字节),重试提交就不会创建第二个、被重复计费的任务。每个业务任务用一个稳定的键——比如从你自己的订单号或任务 ID 派生:
created = client.tasks.create( model="seedance-2.0", input={"prompt": "..."}, idempotency_key="order-8472:video", # 按任务派生出的稳定键)if created.idempotent_replay: print("命中了更早那次请求创建的任务,本次没有新建任务")created, err := client.Tasks.Create(context.Background(), hiapi.CreateParams{ Model: "seedance-2.0", Input: map[string]any{"prompt": "..."}, IdempotencyKey: "order-8472:video",})if err != nil { log.Fatal(err)}if created.IdempotentReplay { log.Println("命中了更早那次请求创建的任务,本次没有新建任务")}CreatedTask created = client.tasks().create( "seedance-2.0", Map.of("prompt", "..."), CreateOptions.builder().idempotencyKey("order-8472:video").build());if (created.isIdempotentReplay()) { System.out.println("命中了更早那次请求创建的任务,本次没有新建任务");}const created = await client.tasks.create({ model: "seedance-2.0", input: { prompt: "..." }, idempotencyKey: "order-8472:video", // 按任务派生出的稳定键});if (created.idempotentReplay) { console.log("命中了更早那次请求创建的任务,本次没有新建任务");}这个保证具体是怎么回事: 同一个键第一次被接受的请求会创建并计费一个任务。用同一个键、同一个请求体重试,都会返回那同一个任务(idempotent_replay 为 true),不会再创建或计费第二个。这个键会在创建后大约 24 小时被清理掉——把它当成”大概一天”,不要当成一个可以精确掐点的边界——清理之后同样的请求会创建一个新任务。键要选和这个任务唯一绑定的值,不要选一个你打算无限期复用的值。
设置了幂等键后,SDK 还会在网络错误时自动重试提交,也会重试 409 IDEMPOTENCY_KEY_PROCESSING(同一个键的首个请求仍在处理中)——但重试次数受客户端配置的重试上限约束,不是无限等待。用同一个键携带不同请求体,会命中下方表格里独立的、不可重试的错误:这说明幂等键的生成逻辑有问题,需要修复,而不是原样重试。
Webhook
Section titled “Webhook”创建任务时传 callback.url(一个公网可访问的 HTTPS 地址),任务无论成功还是失败结束,HiAPI 都会向这个地址发一个 POST——不需要轮询。如果你还在 HiAPI 账户设置里配置了 webhook 签名密钥,这个请求会带签名;在信任它之前,对照原始请求体验签:
# Flask 示例from flask import Flask, requestfrom hiapi import HiAPI, WebhookVerificationError
app = Flask(__name__)app.config["MAX_CONTENT_LENGTH"] = 1024 * 1024 # 1 MiB——读取之前先拒绝超大请求体client = HiAPI(api_key="sk-...", webhook_secret="whsec_...")
@app.post("/hiapi/callback")def callback(): try: task = client.webhooks.verify(request.get_data(), request.headers) except WebhookVerificationError: return "", 400
# 幂等地处理——比如按 task.task_id upsert 你自己的业务记录, # 这样同一事件的重复投递就是无害的。 if task.succeeded and task.output: print(task.output[0].url) return "", 200 # 副作用成功之后再回 2xxpackage main
import ( "io" "log" "net/http"
hiapi "github.com/HiAPIAI/hiapi-go")
var client *hiapi.Client
func main() { var err error client, err = hiapi.New("sk-...", hiapi.WithWebhookSecret("whsec_...")) if err != nil { log.Fatal(err) } http.HandleFunc("/hiapi/callback", handler) log.Fatal(http.ListenAndServe(":8080", nil))}
const maxBodyBytes = 1 << 20 // 1 MiB——HiAPI 的回调体远小于这个数
func handler(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxBodyBytes)) if err != nil { w.WriteHeader(http.StatusRequestEntityTooLarge) return } task, err := client.Webhooks.Verify(body, r.Header, hiapi.VerifyParams{}) if err != nil { w.WriteHeader(http.StatusBadRequest) return }
// 幂等地处理——比如按 task.TaskID upsert 你自己的业务记录, // 这样同一事件的重复投递就是无害的。 if task.Succeeded() && len(task.Output) > 0 { log.Println(task.Output[0].URL) } w.WriteHeader(http.StatusOK) // 副作用成功之后再回 2xx}HiAPI client = HiAPI.builder() .apiKey("sk-...") .webhookSecret("whsec_...") // 与账户设置里配置的密钥一致 .build();
// 在读取请求体之前,先在服务器/框架层面限制请求体大小(比如几百 KB)——// HiAPI 的回调体很小,不该让未认证的调用方能强迫无限读取。byte[] rawBody = readRawRequestBody(); // 不要重新序列化Map<String, String> headers = readRequestHeaders();
try { Task task = client.webhooks().verify(rawBody, headers);
// 幂等地处理——比如按 task.getTaskId() upsert 你自己的业务记录, // 这样同一事件的重复投递就是无害的。 if (task.isSucceeded() && !task.getOutput().isEmpty()) { System.out.println(task.getOutput().get(0).getUrl()); } respond(200, ""); // 副作用成功之后再回 2xx} catch (WebhookVerificationException e) { respond(400, ""); // 签名错误或时间戳过期}// node:http 示例——用框架时,要把**原始**请求体字节交给 verify()// (比如 Express 里用 express.raw({ type: "application/json" }))。import { createServer } from "node:http";import { HiAPI, WebhookVerificationError } from "hiapi_ai";
const client = new HiAPI({ apiKey: "sk-...", webhookSecret: "whsec_..." });const maxBodyBytes = 1024 * 1024; // 1 MiB——验签之前先拒绝超大请求体
async function processTask(task) { // 请替换为按 task.taskId 做键的原子、幂等 upsert。 // 无论终态是 success 还是 fail,都要持久化。 console.log("task:", task.taskId, task.status, task.output[0]?.url);}
async function handleWebhook(rawBody, headers, res) { let task; try { task = client.webhooks.verify(rawBody, headers); } catch (err) { if (err instanceof WebhookVerificationError) { res.writeHead(400).end(); return; } console.error(err); res.writeHead(500).end(); return; }
try { await processTask(task); res.writeHead(200).end(); // 副作用成功之后再回 2xx } catch (err) { console.error(err); res.writeHead(500).end(); // 非 2xx 会让 HiAPI 重新投递 }}
createServer((req, res) => { if (req.method !== "POST" || req.url !== "/hiapi/callback") { res.writeHead(404).end(); return; } const chunks = []; let size = 0; let bodyRejected = false; req.on("data", (chunk) => { if (bodyRejected) return; size += chunk.length; if (size > maxBodyBytes) { bodyRejected = true; res.writeHead(413).end(); req.destroy(); return; } chunks.push(chunk); }); req.on("error", (err) => { console.error(err); if (!res.headersSent) res.writeHead(400).end(); else if (!res.writableEnded) res.destroy(); }); req.on("end", () => { if (bodyRejected || res.writableEnded) return; void handleWebhook(Buffer.concat(chunks), req.headers, res).catch((err) => { console.error(err); if (!res.headersSent) res.writeHead(500).end(); else if (!res.writableEnded) res.destroy(); }); });}).listen(3000);verify 只检查签名、拒绝时间戳超出 300 秒窗口的请求——不会帮你去重。回调按至少一次投递,也可能并发到达,所以要像上面示例那样让处理器本身幂等:副作用按任务 id 落库(比如 upsert),并且副作用成功之后才回 2xx。这样重复投递是无害的,处理失败或进程崩溃则会被自动重投——重投就是你的重试。
有一种写法要避免:处理之前先写一个永久的”已处理”标记,看起来是干净的去重,实际会丢事件——标记写完、处理做完之前进程崩溃,重投会看到”已处理”而永久跳过这个事件。如果确实需要防止两次并发投递重复执行昂贵的副作用,用带过期/租约的”处理中”状态、成功后才标记完成,而不是先写死一个永久标记。
常见失败在各语言里都映射为独立可捕获的错误类型——对照上面选的语言查具体类型名。两条边界要记住:
- 下表的错误码行对应的是同步非 2xx API 响应。任务在轮询过程中失败,永远以轮询失败错误的形式出现——Python 是
TaskFailed,Go 是*TaskFailedError,Java 是TaskFailedException,Node.js 是TaskFailedError——具体原因读它的code字段(可能是TASK_TIMEOUT、STORAGE_UNAVAILABLE等)。 - 没有专属类型的少见响应——比如
402余额不足、403、或重试耗尽后的429——以各 SDK 的基础 API 错误形式出现,带着 HTTP 状态码和原始响应体。
| 场景 | Python | Go | Java | Node.js |
|---|---|---|---|---|
| 401——API Key 缺失或错误 | AuthenticationError | ErrAuthentication | AuthenticationException | AuthenticationError |
| 404——任务不存在,或不属于你 | NotFoundError | ErrNotFound | NotFoundException | NotFoundError |
INVALID_REQUEST——请检查请求参数 | InvalidRequestError | ErrInvalidRequest | InvalidRequestException | InvalidRequestError |
MODEL_UNAVAILABLE——重试或换模型 | ModelUnavailableError | ErrModelUnavailable | ModelUnavailableException | ModelUnavailableError |
TASK_FAILED——提交被同步拒绝 | TaskFailedError | ErrTaskFailedSync | APIException(用 getErrorCode() 判别) | TaskFailedSyncError |
TASK_TIMEOUT——上游任务本身超时(服务端) | TaskTimeoutError | ErrTaskTimeout | TaskTimeoutException | TaskTimeoutError |
STORAGE_UNAVAILABLE——产物存储出错 | StorageUnavailableError | ErrStorageUnavailable | StorageUnavailableException | StorageUnavailableError |
| 503——平台繁忙(自动重试) | ServiceUnavailableError | ErrServiceUnavailable | ServiceUnavailableException | ServiceUnavailableError |
| 409——同一幂等键仍在处理中(在重试上限内自动重试) | IdempotencyKeyProcessingError | ErrIdempotencyKeyProcessing | IdempotencyKeyProcessingException | IdempotencyKeyProcessingError |
| 422——同键携带不同请求体(不可重试) | IdempotencyKeyMismatchError | ErrIdempotencyKeyMismatch | IdempotencyKeyMismatchException | IdempotencyKeyMismatchError |
轮询到的任务终态为 status=fail | TaskFailed | *TaskFailedError | TaskFailedException | TaskFailedError |
run / wait 超过客户端超时时间 | PollTimeout | *PollTimeoutError | PollTimeoutException | PollTimeoutError |
| 网络故障(仅读取类调用自动重试——非幂等提交不重试) | APIConnectionError | *ConnectionError | APIConnectionException | APIConnectionError |
run 创建任务后立即被中止 | — | — | — | RunAbortedError |
run 创建任务后在轮询或执行 onUpdate 时失败 | — | — | — | RunFailedError |
Go 里的 Err* 是配合 errors.Is 使用的哨兵值(sentinel),按这个方式匹配错误类别;需要完整的 *APIError(状态码、错误码、原始响应体)时用 errors.As 取。Node.js 里要注意两个相近的类名:TaskFailedSyncError 是提交被同步拒绝的 TASK_FAILED,TaskFailedError 是轮询到的任务终态为 fail——和 Python 正好相反(Python 里 TaskFailedError 是同步拒绝那个)。Node.js 的 RunAbortedError 和 RunFailedError 都带着已创建任务的 taskId;这个任务可能仍会继续运行并计费,因此要稍后调用 retrieve(taskId),不要重新提交。RunFailedError.cause 会保留打断 run 的轮询、回调或中止错误。429/503 响应,以及幂等调用上的网络错误,都会自动按退避策略重试。完整错误层级与客户端配置详见各 SDK 的 README:Python · Go · Java · Node.js。