Comfy Router 请求排队
提交一次 Comfy Router 运行并轮询其状态,稍后用队列路由或 Python 和 TypeScript SDK 收集或取消该运行。
Comfy Router 请求排队
提交一次 Comfy Router 运行并轮询其状态,稍后用队列路由或 Python 和 TypeScript SDK 收集或取消该运行。
POST /v2/models/{provider}/{model} 会一直保持连接,直到模型完成。队列投递使用相同的模型 ID 和相同的原生请求体,但只要 Router 接纳了本次运行就会立即返回。你会立刻拿到一个 request_id,并在结果就绪时获取它,可以在同一进程中,也可以在另一个进程中。
同步路由有 10 分钟的上限。 Router 的默认截止时间可按部署配置。同步调用一旦达到该时限就会被切断,返回 504 / deadline_exceeded,不论提供商是否仍在工作。如果一次运行可能超过该截止时间,或者调用方无法将连接保持打开那么久,就请使用队列。关于截止时间对计费会说明什么、不会说明什么,请参阅调用会在服务器截止时间被切断。
在以下情况使用队列:一次生成可能超出你能保持的连接时长;Web 请求必须立即返回;你在一个进程中提交、在另一个进程中收集结果;或者你希望同时进行多次生成。排序、接纳、重试、超时、计费和过期都由服务器决定。SDK 只是在其上增加了轮询和易用性封装,别无其他。
选择交付模式
| 同步 | 排队 | |
|---|---|---|
| 路由 | POST /v2/models/{provider}/{model} | POST /v2/models/{provider}/{model}/requests |
| 应答 | 200,返回模型的原生输出 | 201,附带 request_id 和三个 URL |
| 结果 | 在响应中返回 | 稍后收集,输出逐字节相同 |
| 时间限制 | Router 的10 分钟截止时间,随后是 504 / deadline_exceeded | 运行本身没有限制。已完成的结果会保留 24 小时 |
SDK(comfy-sdk 和 @comfyorg/sdk,0.3.0 或更高版本)在 run 旁边将队列暴露为三个方法:
submit(model, body)立即发送请求并返回一个句柄。该句柄提供status()、get()、cancel()以及一个事件迭代器(Python 中为iter_events(),TypeScript 中为events())。subscribe(model, body, ...)在一次调用中完成提交、轮询和收集,并带有进度回调。handle(model, request_id)在另一个进程中根据这两个 ID 重建句柄,不会发起任何调用。
这两个 ID 在任何地方都必不可少,因为它们共同定位该请求: 路由是 /v2/models/{provider}/{model}/requests/{request_id}。
[!NOTE] 这里的events()不是 Comfy Cloud 客户端的job.events()。这里的events()会轮询状态路由,并为 Router 请求产生队列观察结果(状态和队列位置)。Cloud 的那个则是 ComfyUI 工作流任务的实时 SSE 流,携带进度、预览和输出。subscribe()又是第三种东西: 它根本不是迭代器,而是把提交、轮询和收集合并成一次调用。参见events()不是subscribe()。
队列路由
| 路由 | 响应 |
|---|---|
POST /v2/models/{provider}/{model}/requests | 201,包含 request_id、status、queue_position、status_url、response_url、cancel_url |
GET /v2/models/{provider}/{model}/requests/{request_id}/status | 200,包含当前的 status 和 queue_position,以及一个 Retry-After 提示 |
GET /v2/models/{provider}/{model}/requests/{request_id} | 完成后返回 200 及模型的原生输出;未完成时返回 202 及状态响应体 |
PUT /v2/models/{provider}/{model}/requests/{request_id}/cancel | 202 CANCELLATION_REQUESTED,或 409 ALREADY_COMPLETED |
status 为 IN_QUEUE、IN_PROGRESS 或 COMPLETED 之一。不存在单独的失败或已取消状态:未成功的请求会是携带 error_type 的 COMPLETED,因此请根据该字段是否存在来分支处理,而不是根据第四种状态值。SDK 已为你处理这一点:get() 会抛出或拒绝并返回类型化的 Router 错误,而不是把失败作为结果返回。
没有进度事件、webhook 或优先级级别:状态路由就是你跟踪请求的方式。API 参考 包含了每个路由的完整约定。
提交并收集请求
这会排队与快速入门发送的相同请求,并收集图像。请先将你的密钥导出为 COMFY_API_KEY。
from comfy_sdk import Comfy
# 从环境中读取 COMFY_API_KEY。
# 每次 submit() 调用都会生成自己的 Idempotency-Key,并在自动重试时复用它。
with Comfy() as client:
handle = client.models.submit(
"bfl/flux-2-pro",
{"prompt": "a red teapot on a windowsill, morning light"},
)
print("request_id:", handle.request_id) # 配合模型 ID,就是另一个进程所需的全部信息
# 轮询直到请求完成,按服务器指定的 Retry-After 等待。
for update in handle.iter_events():
print(update.status, update.queue_position)
# 提供商自己的负载,与 models.run() 返回的值相同。
# 失败或已取消的请求会在这里抛出类型化的 Router 错误。
result = handle.get()
print("image:", result["result"]["sample"])
import { comfy } from "@comfyorg/sdk";
// 从环境中读取 COMFY_API_KEY。
// 每次 submit() 调用都会生成自己的 Idempotency-Key,并在自动重试时复用它。
type FluxResult = { result: { sample: string } };
const handle = await comfy.models.submit<FluxResult>("bfl/flux-2-pro", {
prompt: "a red teapot on a windowsill, morning light",
});
console.log("requestId:", handle.requestId); // 配合模型 ID,就是另一个进程所需的全部信息
// 轮询直到请求完成,按服务器指定的 Retry-After 等待。
for await (const update of handle.events()) {
console.log(update.status, update.queuePosition);
}
// models.run() 返回的相同结果。失败或已取消的请求会在这里拒绝。
const result = await handle.get();
if (result.kind !== "json") throw new Error("expected a JSON result");
console.log("image:", result.data.result.sample);
BASE="https://api.comfy.org/v2/models/bfl/flux-2-pro"
# 1. 提交。Router 以 201 应答,包含 request_id、status_url、response_url 和 cancel_url。
curl "$BASE/requests" \
-H "X-API-Key: $COMFY_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"prompt": "a red teapot on a windowsill, morning light"}'
# 2. 轮询直到状态为 COMPLETED,每个响应都按指定的 Retry-After 秒数等待。
REQUEST_ID="<request_id from the 201 body>"
curl -i "$BASE/requests/$REQUEST_ID/status" -H "X-API-Key: $COMFY_API_KEY"
# 3. 收集。返回 200 及模型的原生输出;仍在运行时返回 202 及状态正文。
curl "$BASE/requests/$REQUEST_ID" -H "X-API-Key: $COMFY_API_KEY"
# 4. 取消尚未完成的请求。这是请求,不是保证。
curl -X PUT "$BASE/requests/$REQUEST_ID/cancel" -H "X-API-Key: $COMFY_API_KEY"
每个模型页面都会在自己的模型下、排队并稍后收集中带有这种形式,与同步代码片段并列。
在一次调用中跟踪进度并收集
当你确实想要等待,同时也想显示进度时,subscribe 会将提交、轮询和收集合并为一次调用:
def on_update(update):
print(update.status, update.queue_position)
result = client.models.subscribe(
"bfl/flux-2-pro",
{"prompt": "a red teapot on a windowsill, morning light"},
on_queue_update=on_update,
timeout=300,
)
const result = await comfy.models.subscribe<FluxResult>(
"bfl/flux-2-pro",
{ prompt: "a red teapot on a windowsill, morning light" },
{
onQueueUpdate: (update) => console.log(update.status, update.queuePosition),
timeoutMs: 300_000,
},
);
超时是客户端侧的界限,在服务器端没有含义。超时后,subscribe 会在抛出异常前进行一次尽力而为的取消。取消只对尚未开始运行的请求生效:已经在合作伙伴处进行中的生成会完成并被计费,无论是否有人收集它。当请求应当比调用方存活更久时,请使用 submit。
从另一个进程收集
将 request_id 与模型 ID 一起存储。重建句柄两者都需要,在你使用它之前不会发起任何调用。
handle = client.models.handle("bfl/flux-2-pro", request_id)
result = handle.get()
const handle = comfy.models.handle<FluxResult>("bfl/flux-2-pro", requestId);
const result = await handle.get();
检查状态或取消
status() 是一次轮询,返回当前状态。cancel() 请求服务器停止尚未完成的请求。这是一个请求,不是保证:已经在合作伙伴处开始执行的运行可能仍会完成,而下一次 status() 才是真实情况。
update = handle.status()
print(update.status, update.queue_position, update.error_type)
handle.cancel()
const update = await handle.status();
console.log(update.status, update.queuePosition, update.errorType);
await handle.cancel();
异步 Python
AsyncComfy 镜像了每个名称、参数和参数顺序。没有 submit_async,原因与没有 run_async 相同。
from comfy_sdk import AsyncComfy
async with AsyncComfy() as client:
handle = await client.models.submit(
"bfl/flux-2-pro",
{"prompt": "a red teapot on a windowsill, morning light"},
)
async for update in handle.iter_events():
print(update.status, update.queue_position)
result = await handle.get()
SDK 抛出的错误
已完成但未成功的请求会报告为 COMPLETED 并带有 error_type。get() 和 subscribe() 会将其转换为该类别对应的类型化 Router 错误:Python 中是 comfy_sdk.router_exceptions 中的类,TypeScript 中是 routerErrors.*。事件迭代器在这种情况下不会抛出异常,因为它是队列进度的视图:带有 error_type 的完成会作为最后一条观察结果产出,而收集工作由 get() 完成。提交时的 403 not_enabled 会以 NotEnabled 形式到达,并且是终端错误,因此 SDK 不会重试它。
队列响应形状
提交,201。 此时 status 始终为 IN_QUEUE。这三个 URL 都是绝对地址,并使用与提交相同的密钥进行鉴权。
{
"request_id": "6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21",
"status": "IN_QUEUE",
"queue_position": 3,
"status_url": "https://api.comfy.org/v2/models/bfl/flux-2-pro/requests/6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21/status",
"response_url": "https://api.comfy.org/v2/models/bfl/flux-2-pro/requests/6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21",
"cancel_url": "https://api.comfy.org/v2/models/bfl/flux-2-pro/requests/6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21/cancel"
}
request_id 同时也是提交请求的 X-Comfy-Request-Id 头的值。请把它与模型 ID 放在一起:该请求需要二者共同定位。
状态,200。 形状相同,但包含当前状态。queue_position 统计排在你前面的请求数量,当运行位于队首时其值为 0。此响应上的 Retry-After 是 Router 对何时值得再次轮询一次往返的预估。它只是提示,不是硬性限制,排在队列末尾的请求会被要求等待得比已在运行的请求更久。轮询得再快也不会更早获知任何信息,只会消耗你自己的速率限制额度。
{
"request_id": "6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21",
"status": "IN_PROGRESS",
"queue_position": 0,
"status_url": "...",
"response_url": "...",
"cancel_url": "..."
}
一个已完成但未成功的请求为 COMPLETED,并带有 error_type,其中携带的粗粒度分类与读取结果时放在 X-Comfy-Error-Type 上的相同。该字段在成功时是缺失的,而不是 null。
{
"request_id": "6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21",
"status": "COMPLETED",
"error_type": "content_policy_violation",
"status_url": "...",
"response_url": "...",
"cancel_url": "..."
}
结果。 200 携带模型自身的原生输出,与同一模型和输入的同步路由所返回的内容逐字节一致,并使用提供商自己的 Content-Type。当请求尚未完成时,读取会返回 202 和上面的状态响应体,因此只轮询结果 URL 的客户端只需解析一种类型。失败的请求会作为错误响应返回,并设置 X-Comfy-Error-Type,其分类与同步路由相同。
取消。 202 加 CANCELLATION_REQUESTED 表示取消请求已被接受,并不表示运行已经停止。已经发往合作伙伴处运行的请求仍可能完成,而合作伙伴已完成的一次生成无论是否有人取回都会被计费。之后请读取状态:生效的取消会显示为 COMPLETED,并带有 error_type: cancelled。在能够运行之前就已超时的请求会以同样方式显示 queue_timeout。已经完成的请求会返回 409,并带有 ALREADY_COMPLETED。
幂等性与计费
- 与同步路由相同的收费。 提供商向 Comfy 计费时才会向你计费。在队列中等待所花费的时间不计费。
- 每次提交使用一个
Idempotency-Key。 SDK 会为每次submit调用生成一个新密钥,因此对同一输入的两次有意提交就是两个请求。同一次调用在同一密钥下的重试不会排入第二次运行:它会返回原始句柄,并带有Idempotent-Replayed: true。当响应丢失可能让你损失request_id时,请传入你自己的密钥。参见请求头。 - 结果会过期。 已完成请求在完成后会保留 24 小时。之后,状态和结果的读取会返回
410,结果已不复存在。请及时收集并下载输出所携带的任何资产 URL。 - 轮询也是请求。 状态和结果的读取都会计入每个调用方的请求速率。请遵守
Retry-After,而不要以固定的短间隔持续轮询。
队列错误响应
| 状态 | X-Comfy-Error-Type | 含义 |
|---|---|---|
402 | insufficient_credits | 工作区无法为该次运行提供资金。不会入队任何内容,也不会扣费;充值后可以重新发送同一个 Idempotency-Key。 |
403 | not_enabled | 该调用方无法使用队列:背后没有工作区的密钥(早于工作区出现的传统密钥)、自带密钥请求,或队列无法运行的模型。对该请求而言这是终态,请勿重试。 |
404 | model_not_found | {provider}/{model} ID 无法解析到任何 Router 模型。 |
404 | request_not_found | 该调用方和该模型下不存在具有此 ID 的请求。 |
409 | concurrency_limit_exceeded | 同一个 Idempotency-Key 仍在准入过程中。等待 Retry-After 后重新发送同一个密钥,即可获得原始句柄。 |
409 | invalid_input | 该 Idempotency-Key 已被另一个不同的请求占用。请用新的密钥发送此请求。 |
409 | 响应体中的 ALREADY_COMPLETED | 仅适用于取消路由:该请求已经完成,因此没有什么可取消的。 |
410 | 该请求曾经存在,但已超出其保留窗口,即完成 24 小时之后。对此 ID 而言是永久性的。 | |
422 | invalid_input | 模型拒绝了该输入。响应体携带逐字段的详细信息,与同步路由完全一致。 |
每个错误响应都携带 X-Comfy-Request-Id。联系支持时请附上该 ID。