Skip to main content
Comfy Router 尚未全面可用。 下文引用的路由: POST /v1/models/{provider}/{model} 及其目录和架构相关端点,目前尚未提供服务请求:经过身份验证的调用现在会返回 404。本页描述的是它们将要提供的契约,并在该上线之前发布,以便集成可以针对已知形状进行编写。以下所有内容都是关于该契约的说明,而不是你现在可以实践的行为。
Comfy Router 是一次同步调用:你使用一个凭据,将合作伙伴模型的原生输入发送到一个主机,连接保持打开,200 响应携带该模型的原生输出。这种形状正是让首次集成变得简短的原因,也是本页所有限制的来源。在围绕 Router 进行设计之前阅读本页,而不是之后:下文大部分内容都有直接的替代方案,而那些没有替代方案的内容,值得你在基于 Router 并不成立的假设进行构建之前了解。

概览

每一行都会链接到详细说明该限制的章节。设计使然表示该限制是 Router 工作方式的一部分,不依赖任何待实现的功能;尚未支持表示 Router 预计将获得该能力,但本页面不对具体时间做出任何承诺。

不支持排队提交

运行模型只有一种方式:POST /v1/models/{provider}/{model},该端点会保持连接,直到生成完成并在响应中返回结果。没有接受任务后返回标识符、让你稍后再来获取结果的端点,也没有完成时的回调或 webhook。计划中有一个对应的排队端点,在 API 参考中写作 /v1/queue/models/{provider}/{model};它目前不属于契约的一部分,对它发起调用不会被处理。 替代做法。 对大多数模型来说这不成问题:保持连接打开并读取结果即可。快速的图像模型几秒钟即可返回;长时间的视频生成可能运行数分钟,Router 会为其一直保持连接。设置一个宽裕的客户端读取超时,要高于 Router 自身的截止时间,并将该调用视为长时间运行的请求,而非快速请求。如果你的架构确实无法保持连接打开,例如执行上限很短的 serverless 函数,或你预期用户会关闭的浏览器标签页,那么就从你能控制且能够保持连接的 worker 发起调用,或者使用合作伙伴代理路由,选择提供自身提交和轮询机制的提供商。参见最后一节 状态:尚未提供。 排队路径属于预期功能;本页不承诺具体时间。

响应中不包含费用或积分数字

Router 响应会告诉你模型生成了什么,但它的契约不涉及任何成本信息。响应体中不包含收费金额、积分余额或使用量数字,路由也不声明任何成本标头。有一个注意事项,以免你感到意外:Router 与合作伙伴代理路由共享同一条计费路径,该路径在允许列表中的提供商的计费响应上标记 X-Comfy-Credits-Used,因此当你调用其中某个提供商时,该标头可能会出现在 Router 响应中。它不属于 Router 契约的一部分:对于允许列表之外的每个提供商,该标头都不会出现,并且它特意不会在幂等重试时重放,其目的正是为了防止汇总该标头的客户端对仅支付过一次的调用进行重复计数。不要基于它来对账。模型目录也是如此:它包含调用方在调用之前所需的计费事实,而从不包含价格。因此,你无法仅凭 Router 响应来核对支出,也无法在未从其他地方获得 X 的情况下向用户显示“这次调用花了 X”。 替代做法。 你的余额、使用量和账单都保存在 Comfy 平台的 platform.comfy.org:这是你已支出和剩余金额的事实来源,且不受本页任何内容的影响。Router 在调用时确实会告诉你两件值得利用的事情:因积分不足而被拒绝的调用会返回 insufficient_credits,这样你就可以将积分耗尽作为类型化错误来处理,而无需预先检查余额;另外,每个模型的目录条目都带有 billing.charges_on_policy_rejection,它表明该特定模型是否会基于内容政策拒绝生成并因此向你收费。它是一个包含三个值的字符串,而不是布尔值:yesnounknown。请将 unknown 理解为“这可能会向你收费”:它意味着还没有人确定该模型的行为,而它存在正是为了使未经检查的模型不会以 no 形式发布,因为 no 是一种断言。该字段刻意不是 enum,因此请将任何你无法识别的值也视为 unknown,并且不要对它进行真值检查:字符串 "no" 在大多数语言中是真值,这样的检查会让这个字段本来要捕获的情况反转。不同提供商在这方面存在差异,这种差异在调用时不可见,而在调用之前读取它,正是为了避免事后出现无法解释的收费。 状态:尚未提供逐次调用的数字。请注意,目录刻意不携带价格:定价属于维护价格的地方,而不会重复复制到可能与之偏离的模型列表中。

无法恢复已丢失的调用

Router 不会保留进行中调用的可恢复记录。没有状态路由,没有任务标识符,也没有任何可重新连接的对象。如果连接在调用中途断开(客户端崩溃、网络分区、重启进程的部署),响应便不复存在,之后你也无从查询这次调用。生成是否已完成并被计费,与你是否收到它,是两个不同的问题,而丢失连接并不能可靠地回答其中任何一个。 应该怎么做。 在每次调用中发送 Idempotency-Key 请求头。它不能让已丢失的调用恢复,但能让重试变得安全。Router 会在调用时长内保留该密钥;当调用确实将答案送达你时,Router 会将该响应记在该密钥下并保留 24 小时。使用相同密钥重试时,便会重放已记录的响应,而不是第二次向提供商分派请求(并再次计费),并标记为 Idempotent-Replayed: true,以便你区分重放与全新运行。请为每个逻辑调用生成一个新密钥,而不是每次尝试都生成新密钥。以不同请求体出示相同密钥会返回 409,而不是静默覆盖。 请准确理解这能给你带来什么,因为这是计费属性,而不是投递属性:一个密钥最多只计费一次。 它并不是承诺一个密钥最多只向提供商分派一次。Router 会为你实际收到的答案保留密钥;任何未向你收费的结果都会释放密钥,使调用可以再次进行。5xx408/425/429,以及(这里最关键的一种情况)完全没有内容到达你的调用:这些情况都会释放密钥,使用该密钥重试会真正重新执行,并重新分派给提供商。 因此,连接断开正是幂等性无法挽救的情况。 调用中途丢失连接通常意味着从未有任何响应真正交付给你,这正是上述的释放路径:使用相同密钥重试会开启全新运行,而不是把你错过的结果交给你。如果原始生成已经被分派,提供商可能会第二次运行它。这是正确的默认行为:你从未收到且未被计费的调用应当可以重新运行。但请按“重试会产生新运行”来规划,而不是“重试会找回丢失的运行”。 当 Router 确实为密钥保留了内容时,重试会得到应答而不是重新执行:要么重放原始响应,要么返回 409 说明无法重放的原因。在原始调用仍在进行中时发送的重试会返回携带 Retry-After409,因此请等待后再重新发送相同的密钥。针对已完成但其响应 Router 无法保留忠实副本的调用进行重试,也会返回 409。这不仅限于响应过大的情况:超过重放上限的响应、应答后失败或崩溃的处理器,以及向你写入时失败或写入不足,都会将密钥记录为已消费但不可重放,并返回相同的 409。看到这个错误时,不要去找大小问题。上述所有情况下的引导都是一样的:使用密钥。原始调用已完成并被计费,Router 既不会凭空捏造其响应,也不会在旧密钥下重新运行它。
已生成的合同中尚无这些内容。 此处描述的 Idempotency-Key 请求头、409 响应以及 Idempotent-ReplayedRetry-After 响应头,并未在生成参考文档所依据的 OpenAPI 合同中的 POST /v1/models/{provider}/{model} 上声明,因此它们不会出现在生成的 API 参考中,SDK 也不会对它们进行建模。在获得支持之前,请自行发送和读取这些内容。
状态:尚未支持。 持久化、可恢复的执行预计将随队列式路径一同推出,届时请求记录将有处可存。幂等重试目前就是答案,而且它不是临时方案:无论如何都值得集成。

调用会在服务器截止时间被切断

一次 Router 调用可将连接保持 10 分钟。这是默认值;它是服务器端配置值,而不是固定常量,因此应将其视为设计时依据的数字,而不是合同里写死的保证。超过该时间后,Router 不再等待,会取消自身发送给提供商的进行中请求,并以 X-Comfy-Error-Type: deadline_exceeded 返回 504deadline_exceeded 调用不会被计费:这个上限是我们的,所以它的成本也是我们的。 取消操作不会做的两件事,都值得你在重试之前了解。它不会撤销提供商已经接受的生成:对于 Router 通过提交任务并轮询来驱动的合作伙伴,截止时间到期只会结束 Router 自身的等待,而不会结束提供商的工作,因此该任务可能运行到完成,而重试可能产生第二次生成(你仍然不会为超时的调用付费)。它也无法撤回已发送的应答:如果处理程序在截止时间到期的瞬间赢得竞态并提交了响应,你会保留该响应,而不是 504 不要将它和另一个 504 混淆。provider_timeout 表示合作伙伴未能及时应答,而这种错误被计费;deadline_exceeded 表示 Router 自身的上限已过期。两种原因,两种计费结果,这正是它们在同一个状态码上分成两个分类的原因:请根据 X-Comfy-Error-Type 分支判断,切勿只依赖状态码。 替代做法。 将客户端的读取超时设置为留足余量地高于截止时间,而不是低于它。先放弃的客户端会把带有请求标识符的类型化 504 变成一次不透明的本地中止,而你也会丢失支持团队唯一可以追踪的线索。如果单个生成确实无法在截止时间内完成,那么 Router 目前并不是适合它的正确形式:请通过合作伙伴代理路由运行它,该路由会提交并轮询;或者将工作拆分成每次都能在截止时间内完成的多次调用。 状态:有意为之。 必须存在一个上限:如果没有上限,卡住的上游会无限期地占用连接和并发槽位。具体数值可以调整,但截止时间的存在不会消失。

调用运行期间无进度

POST /v1/models/{provider}/{model} 只在调用结束时返回一次。没有流式响应、没有服务器发送事件、没有百分比、没有部分帧或预览帧。即使对于自身 API 为“提交并轮询”(submit-and-poll)的合作伙伴,情况也是如此:Router 会在您的这一次调用内部完成该轮询,但它看到的中间状态不会转发给您。从外部来看,耗时三秒的图像与耗时六分钟的视频形状相同:一个请求、一个响应,中间什么也没有。 替代做法。 就目前的 Router 而言,没有任何办法:请显示不确定的进度状态,而不是一个您无法获取的百分比。如果进度是某个特定提供商的硬性要求,请检查该提供商的合作伙伴代理路由是否公开了它们自己的轮询或流式接口,并直接使用这些路由:少数提供商确实如此,这些路由未做改动且完全受支持。 状态:尚未实现,并且与排队路径相关联:进度需要有一个可以反馈的去处,排队提交提供了这一去处,而单个同步调用无法提供。

三个预告的分类不在词汇表中

Router 的 error_type 词汇表是一个包含十五个分类的封闭集合:即 API 参考 中列出且快速入门所指的那十五个。该参考文档的正文中还另行提及三个作为预期新增项:file_download_errorcancelledqueue_timeout。它们只是被提及,仅此而已。它们目前不是该集合的成员:没有任何 Router 响应携带它们,根据契约生成的客户端也不认识它们,而且如果 Router 在内部收到其中一个,它会用 internal_error 代替,而不是将其作为响应发出。因此,你今天为它们编写的分支是永远不会运行的分支,而且它们在参考文档中的出现并不能证明 Router 会取消调用或将它们排队:它两者都不会做。 它们以书面形式预告出来,而不是被完全省略,因为 error_type 刻意设计为普通字符串而非 enum,而一个硬性拒绝无法识别分类的客户端,恰恰会在事情已经出错的时候失败得最严重。提前说出这些新增项,正是为了让读者知道该集合在设计上就是开放式的。 应该怎么做。 处理 Router 实际发布的十五个分类,完整列表见 API 参考,并编写一个将任何无法识别的值视为 internal_error 的回退分支。这个回退分支就是整套机制:正是它让这三个分类,以及任何在你编写客户端之后新增的分类,在到达时都不会导致你的客户端出错。控制流应基于粗粒度分类进行分支;当你需要具体原因时,再读取 422 响应体中每个字段对应的 type 状态:尚未实现。 这三个分类各自对应 Router 尚不具备的行为,而且每个分类都会在开始输出它的同一次变更中加入词汇表,绝不会在此之前加入。

Router 并不覆盖每个合作伙伴操作

Router 运行合作伙伴的模型。它并不承接合作伙伴暴露的每个操作:文件上传、账户与资产读取、提供商特定的管理调用、流式聊天端点,以及某些合作伙伴发布的提交与轮询配对。Router 也不会重塑其中任何一项:它转发模型的原生输入,并原样返回其原生输出,因此不存在可将不受支持的操作移植上去的统一封装格式。 替代做法。 /proxy/… 下的合作伙伴代理路由在同一主机上、使用同一凭证仍然完全受支持,它们正是 Router 未覆盖的任何内容的答案。它们没有被弃用,也没有处于逐步淘汰的路径上;在同一集成中与 Router 一起使用它们属于预期用法,而非变通方案。当你想跨许多模型只使用一种路由形状和一个凭证时,请使用 Router;当你需要某个特定的合作伙伴操作、提供商自身的流式响应,或 Router 刻意隐藏的提交与轮询控制时,请使用 /proxy/… 状态:有意为之。 Router 刻意收窄了暴露面:单一路由形状本身就是其特性。代理暴露面保持现状。

下一步