> ## Documentation Index
> Fetch the complete documentation index at: https://comfyuiwiki.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Comfy Router 请求排队

> 提交一次 Comfy Router 运行并轮询其状态，稍后用队列路由或 Python 和 TypeScript SDK 收集或取消该运行。

`POST /v2/models/{provider}/{model}` 会一直保持连接，直到模型完成。队列投递使用相同的模型 ID 和相同的原生请求体，但只要 Router 接纳了本次运行就会立即返回。你会立刻拿到一个 `request_id`，并在结果就绪时获取它，可以在同一进程中，也可以在另一个进程中。

**同步路由有 10 分钟的上限。** Router 的默认截止时间可按部署配置。同步调用一旦达到该时限就会被切断，返回 `504` / `deadline_exceeded`，不论提供商是否仍在工作。如果一次运行可能超过该截止时间，或者调用方无法将连接保持打开那么久，就请使用队列。关于截止时间对计费会说明什么、不会说明什么，请参阅[调用会在服务器截止时间被切断](/zh/development/comfy-router/limitations#调用会在服务器截止时间被中断)。

在以下情况使用队列：一次生成可能超出你能保持的连接时长；Web 请求必须立即返回；你在一个进程中提交、在另一个进程中收集结果；或者你希望同时进行多次生成。排序、接纳、重试、超时、计费和过期都由服务器决定。SDK 只是在其上增加了轮询和易用性封装，别无其他。

## 选择交付模式

|      | 同步                                                                                                           | 排队                                            |
| ---- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------- |
| 路由   | `POST /v2/models/{provider}/{model}`                                                                         | `POST /v2/models/{provider}/{model}/requests` |
| 应答   | `200`，返回模型的原生输出                                                                                              | `201`，附带 `request_id` 和三个 URL                 |
| 结果   | 在响应中返回                                                                                                       | 稍后收集，输出逐字节相同                                  |
| 时间限制 | Router 的[10 分钟截止时间](/zh/development/comfy-router/limitations#调用会在服务器截止时间被中断)，随后是 `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()`](/zh/development/api-development/sdks#events-不是-subscribe)。
</Note>

## 队列路由

| 路由                                                               | 响应                                                                                       |
| ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `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 参考](/zh/development/comfy-router/reference#端点) 包含了每个路由的完整约定。

## 提交并收集请求

这会排队与[快速入门](/zh/development/comfy-router/quickstart)发送的相同请求，并收集图像。请先将你的密钥导出为 `COMFY_API_KEY`。

<CodeGroup>
  ```python Python theme={null}
  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"])
  ```

  ```typescript TypeScript theme={null}
  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);
  ```

  ```bash cURL theme={null}
  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"
  ```
</CodeGroup>

每个[模型页面](/zh/development/comfy-router/models)都会在自己的模型下、**排队并稍后收集**中带有这种形式，与同步代码片段并列。

### 在一次调用中跟踪进度并收集

当你确实想要等待，同时也想显示进度时，`subscribe` 会将提交、轮询和收集合并为一次调用：

<CodeGroup>
  ```python Python theme={null}
  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,
  )
  ```

  ```typescript TypeScript theme={null}
  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,
    },
  );
  ```
</CodeGroup>

超时是客户端侧的界限，在服务器端没有含义。超时后，`subscribe` 会在抛出异常前进行一次尽力而为的取消。取消只对尚未开始运行的请求生效：已经在合作伙伴处进行中的生成会完成并被计费，无论是否有人收集它。当请求应当比调用方存活更久时，请使用 `submit`。

### 从另一个进程收集

将 `request_id` 与模型 ID 一起存储。重建句柄两者都需要，在你使用它之前不会发起任何调用。

<CodeGroup>
  ```python Python theme={null}
  handle = client.models.handle("bfl/flux-2-pro", request_id)
  result = handle.get()
  ```

  ```typescript TypeScript theme={null}
  const handle = comfy.models.handle<FluxResult>("bfl/flux-2-pro", requestId);
  const result = await handle.get();
  ```
</CodeGroup>

### 检查状态或取消

`status()` 是一次轮询，返回当前状态。`cancel()` 请求服务器停止尚未完成的请求。这是一个请求，不是保证：已经在合作伙伴处开始执行的运行可能仍会完成，而下一次 `status()` 才是真实情况。

<CodeGroup>
  ```python Python theme={null}
  update = handle.status()
  print(update.status, update.queue_position, update.error_type)

  handle.cancel()
  ```

  ```typescript TypeScript theme={null}
  const update = await handle.status();
  console.log(update.status, update.queuePosition, update.errorType);

  await handle.cancel();
  ```
</CodeGroup>

### 异步 Python

`AsyncComfy` 镜像了每个名称、参数和参数顺序。没有 `submit_async`，原因与没有 `run_async` 相同。

```python theme={null}
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 都是绝对地址，并使用与提交相同的密钥进行鉴权。

```json theme={null}
{
  "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 对何时值得再次轮询一次往返的预估。它只是提示，不是硬性限制，排在队列末尾的请求会被要求等待得比已在运行的请求更久。轮询得再快也不会更早获知任何信息，只会消耗你自己的速率限制额度。

```json theme={null}
{
  "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`。

```json theme={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` 时，请传入你自己的密钥。参见[请求头](/zh/development/comfy-router/headers)。
* **结果会过期。** 已完成请求在完成后会保留 24 小时。之后，状态和结果的读取会返回 `410`，结果已不复存在。请及时收集并下载输出所携带的任何资产 URL。
* **轮询也是请求。** 状态和结果的读取都会计入[每个调用方的请求速率](/zh/development/comfy-router/limitations#请求按调用方进行速率限制)。请遵守 `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。

## 后续

* [错误与重试](/zh/development/comfy-router/errors)：了解错误响应，并在不产生二次扣费的情况下重试。
* [API 参考](/zh/development/comfy-router/reference#端点)：每条队列路由的完整契约。
