diff --git a/api-reference/comfy-router/limitations.mdx b/api-reference/comfy-router/limitations.mdx new file mode 100644 index 000000000..a7ff1f796 --- /dev/null +++ b/api-reference/comfy-router/limitations.mdx @@ -0,0 +1,106 @@ +--- +title: "Comfy Router limitations" +description: "What Comfy Router does not do today, what to use instead where an alternative exists, and which of those limits are expected to change." +--- + + +**Comfy Router is not generally available yet.** The routes referenced below — +`POST /v1/models/{provider}/{model}` and its catalog and schema siblings — are +not serving requests yet: an authenticated call answers `404` today. This page +describes the contract they will serve, published ahead of that rollout so an +integration can be written against a known shape. Everything below is a +statement about that contract, not about behaviour you can exercise right now. + + +Comfy Router is one synchronous call: you send a partner model's native input to one host with one credential, the connection stays open, and a `200` carries that model's native output. That shape is what makes the first integration short, and it is also where every limit on this page comes from. Read this page before you design around Router, not after — most of what follows has a straightforward alternative, and the ones that do not are worth knowing before you build on an assumption Router does not hold. + +## At a glance + +Each row links to the section that explains it. **Deliberate** means the limit is part of how Router works and is not waiting on anything; **not yet** means Router is expected to gain the capability, though this page makes no commitment about when. + +| Limitation | Use instead | Status | +| --- | --- | --- | +| [No queued submission — the call is synchronous](#no-queued-submission) | Hold the connection open, or use a partner-proxy route that submits and polls | Not yet | +| [No cost or credit figures on a response](#no-cost-or-credit-figures-on-a-response) | Read your balance and usage on the Comfy platform; check `billing` on the model's catalog entry before calling | Not yet | +| [No way to resume a call you lost](#no-way-to-resume-a-call-you-lost) | Send `Idempotency-Key`, so a retry is charged at most once — it does not recover the lost result | Not yet | +| [Calls are cut off at a server deadline](#calls-are-cut-off-at-a-server-deadline) | Give your client a timeout above the deadline; split work that cannot finish inside it | Deliberate | +| [No progress while a call runs](#no-progress-while-a-call-runs) | Nothing on Router today; a partner-proxy route may expose its own progress | Not yet | +| [Three forecast buckets are not in the vocabulary](#three-forecast-buckets-are-not-in-the-vocabulary) | Handle the fifteen buckets Router publishes; treat anything unrecognized as `internal_error` | Not yet | +| [Router does not cover every partner operation](#router-does-not-cover-every-partner-operation) | The partner-proxy routes under `/proxy/…` on the same host | Deliberate | + +## No queued submission + +There is one way to run a model: `POST /v1/models/{provider}/{model}`, which holds the connection until the generation finishes and returns the result in the response. There is no endpoint that accepts a job, hands you an identifier and lets you collect the result later, and no callback or webhook on completion. A queued counterpart is planned and is referenced in the API reference as `/v1/queue/models/{provider}/{model}`; it is not part of the contract today, and a call to it is not served. + +**What to do instead.** For most models this is a non-issue: keep the connection open and read the result. A fast image model returns in a few seconds; a long video generation can run for minutes, and Router will hold the connection for it. Set a generous client read timeout — above [Router's own deadline](#calls-are-cut-off-at-a-server-deadline) — and treat the call as long-running rather than as a fast request. If your architecture genuinely cannot hold a connection open — a serverless function with a short execution ceiling, a browser tab you expect the user to close — then run the call from a worker you control that can, or use a partner-proxy route for a provider that exposes its own submit-and-poll pair. See [the last section](#router-does-not-cover-every-partner-operation). + +**Status: not yet.** The queued path is expected; nothing on this page commits to when. + +## No cost or credit figures on a response + +A Router response tells you what the model produced, and its contract says nothing about what it cost. There is no charge amount, no credit balance and no usage figure in the body, and the route declares no cost header. One caveat, so it does not surprise you: Router shares a billing path with the partner-proxy routes, and that path stamps `X-Comfy-Credits-Used` on a billed response for an allowlist of providers, so the header can appear on a Router call to one of them. It is not part of Router's contract — it is absent for every provider outside that allowlist, and it is deliberately *not* replayed on an idempotent retry, precisely so a client summing it cannot double-count a call that was only paid for once. Do not build reconciliation on it. The model catalog is the same: it carries billing *facts* a caller needs before invoking, never prices. So you cannot reconcile spend from a Router response alone, and you cannot show a user "this call cost X" without getting X from somewhere else. + +**What to do instead.** Your balance, your usage and your invoices live on the Comfy platform at [platform.comfy.org](https://platform.comfy.org) — that is the source of truth for what you have spent and what you have left, and it is unaffected by anything on this page. Two things Router does tell you at call time are worth using: a call refused for lack of credit comes back as `insufficient_credits`, so you can handle exhaustion as a typed error rather than by pre-checking a balance; and each model's catalog entry carries `billing.charges_on_policy_rejection`, which says whether that specific model charges you for a generation it then refuses on content-policy grounds. It is a **string with three values**, not a boolean: `yes`, `no` and `unknown`. Read `unknown` as "this might charge you" — it means nobody has established that model's behaviour yet, and it exists precisely so an unchecked model is not published as a `no`, which is a claim. The field is deliberately not an `enum`, so treat any value you do not recognize as `unknown` too, and do not write a truthiness check over it: the string `"no"` is truthy in most languages, and that check gets backwards the one case it exists to catch. Providers differ on that, the difference is invisible at call time, and reading it before you call is how you avoid a charge you cannot explain afterwards. + +**Status: not yet** for per-call figures. Note that the *catalog* deliberately carries no prices — pricing belongs where pricing is maintained, not duplicated into a model listing that would drift from it. + +## No way to resume a call you lost + +Router does not keep a resumable record of an in-flight call. There is no status route, no job identifier, and nothing to reconnect to: if the connection drops mid-call — a client crash, a network partition, a deploy that restarts your process — the response is gone, and the call is not something you can ask about afterwards. Whether the *generation* completed and was charged is a separate question from whether you received it, and losing the connection does not reliably answer either. + +**What to do instead.** Send an `Idempotency-Key` header on every call. It does not make a lost call resumable, but it makes retrying one safe. Router reserves the key for the duration of the call, and when the call actually reached you with an answer it records that response against the key for 24 hours; retrying with the **same** key then replays the recorded response instead of dispatching — and re-charging — the provider a second time, marked `Idempotent-Replayed: true` so you can tell a replay from a fresh run. Generate a fresh key per logical call, not per attempt; the same key presented with a *different* request body is a `409` rather than a silent overwrite. + +Be precise about what that buys you, because it is a **billing** property and not a delivery one: **a key is charged at most once.** It is not a promise that a key is dispatched to the provider at most once. Router holds a key against an answer you actually received; the outcomes that charged you nothing release it so the call can be made again. A `5xx`, a `408`/`425`/`429`, and — this is the one that matters here — a call where nothing reached you at all: each of those releases the key, and a retry with it genuinely re-runs and re-dispatches the provider. + +**So a dropped connection is the case idempotency does *not* rescue.** A connection lost mid-call usually means no response was ever committed to you, which is exactly the release path above: retrying with the same key starts a fresh run rather than handing you the result you missed, and if the original generation had already been dispatched the provider may run it a second time. That is the right default — an unbilled call you never received should be re-runnable — but plan for "retry produces a new run", not "retry collects the lost one". + +When Router *does* hold something for the key, the retry is answered rather than re-run: either the original response replayed, or a `409` explaining why it cannot be. A retry sent while the original is still in flight is a `409` carrying `Retry-After`, so wait and re-send the same key. A retry against a call that completed but whose response Router could not keep a faithful copy of is also a `409` — and that is not only the oversized-response case: a response past the replay cap, a handler that failed or panicked after answering, and a write to you that failed or came up short all record the key as consumed-but-not-replayable and return the same `409`. Do not go hunting for a size problem when you see it. The guidance in every one of those cases is the same: use a **new** key. The original completed and was charged, and Router will neither invent its response nor re-run it under the old key. + + +**Not yet in the generated contract.** The `Idempotency-Key` request header, the `409` response and the `Idempotent-Replayed` and `Retry-After` response headers described here are not declared on `POST /v1/models/{provider}/{model}` in the OpenAPI contract the reference is generated from, so they do not appear in the generated API reference and the SDKs do not model them. Send and read them yourself until they do. + + +**Status: not yet.** Durable, resumable execution is expected to arrive with the queued path, which is where a request record has somewhere to live. Idempotent retry is the answer today and is not a stopgap — it is worth wiring in regardless. + +## Calls are cut off at a server deadline + +One Router call may hold its connection for **10 minutes**. That is the default; it is a server-side configuration value rather than a fixed constant, so treat it as the number to design against rather than a guarantee etched into the contract. Past it, Router stops waiting, cancels its own in-flight request to the provider and answers `504` with `X-Comfy-Error-Type: deadline_exceeded`. **A `deadline_exceeded` call is not billed** — the bound is ours, so its cost is ours. + +Two things that cancellation does not do, both worth knowing before you retry. It does not recall a generation a provider has already accepted: for the partners Router drives by submitting a job and polling it, expiring the deadline ends Router's own wait, not the provider's work, so that job can run to completion and a retry can produce a **second generation** (you are still not billed for the timed-out call). And it cannot un-send an answer: if the handler wins the race and commits a response just as the bound expires, you keep that response rather than the `504`. + +Do not confuse it with the other `504`. `provider_timeout` is the partner failing to answer in time, and that one **is** billed; `deadline_exceeded` is Router's own bound expiring. Two causes, two billing outcomes, which is exactly why they are two buckets on the same status code — branch on `X-Comfy-Error-Type`, never on the status alone. + +**What to do instead.** Set your client's read timeout comfortably *above* the deadline, not below it. A client that gives up first turns a typed `504` with a request identifier into an opaque local abort, and you lose the one artifact support can trace. If a single generation genuinely cannot finish inside the deadline, Router is not the right shape for it today: run it through a partner-proxy route that submits and polls, or break the work into calls that each finish inside the bound. + +**Status: deliberate.** A bound has to exist — without one, a stuck upstream holds a connection and a concurrency slot indefinitely. The specific number may be tuned; the existence of a deadline will not go away. + +## No progress while a call runs + +`POST /v1/models/{provider}/{model}` returns exactly once, at the end. There is no streaming response, no server-sent events, no percentage, no partial or preview frame. This holds even for partners whose own API is submit-and-poll: Router does that polling internally, inside your one call, and the intermediate states it sees are not forwarded to you. From the outside, a three-second image and a six-minute video are the same shape — one request, one response, nothing in between. + +**What to do instead.** On Router today, nothing: show an indeterminate progress state rather than a percentage you cannot source. If progress is a hard requirement for a specific provider, check whether that provider's partner-proxy routes expose their own polling or streaming and use those directly — a few do, and they are unchanged and fully supported. + +**Status: not yet**, and tied to the queued path: progress needs somewhere to report *to*, which a queued submission provides and a single synchronous call does not. + +## Three forecast buckets are not in the vocabulary + +Router's `error_type` vocabulary is a **closed set of fifteen** buckets — the fifteen the [API reference](/api-reference/comfy-router/reference) lists and the quickstart points at. Three more are named in that reference's prose as expected additions: `file_download_error`, `cancelled` and `queue_timeout`. They are named, and that is all they are. They are **not members of the set today**: no Router response carries one, a client generated from the contract does not know them, and if Router were handed one internally it substitutes `internal_error` rather than putting it on the wire. So a branch you write for them today is a branch that never runs, and their appearance in the reference is not evidence that Router cancels calls or queues them — it does neither. + +They are forecast in writing rather than left out entirely because `error_type` is deliberately a plain string and not an `enum`, and a client that hard-rejects an unrecognized bucket fails hardest exactly when something has already gone wrong. Naming the additions in advance is how a reader knows the set is open-ended by design. + +**What to do instead.** Handle the fifteen buckets Router actually publishes, listed in full in the [API reference](/api-reference/comfy-router/reference), and write one fallback branch that treats any unrecognized value as `internal_error`. That fallback is the whole mechanism: it is what lets these three, and any bucket added after your client was written, arrive without breaking you. Branch on the coarse bucket for control flow, and read the per-field `type` inside a `422` body when you need the specific reason. + +**Status: not yet.** Each of the three corresponds to behaviour Router does not have yet, and each joins the vocabulary in the same change that starts emitting it — never before. + +## Router does not cover every partner operation + +Router runs partner *models*. It does not front every operation a partner exposes — the file uploads, the account and asset reads, the provider-specific management calls, the streaming chat endpoints and the submit-and-poll pairs that some partners publish. Nor does Router reshape any of them: it forwards a model's native input and returns its native output unchanged, so there is no unified envelope to port an unsupported operation onto. + +**What to do instead.** The partner-proxy routes under `/proxy/…` remain fully supported on the same host, with the same credential, and they are the answer for anything Router does not cover. They are not deprecated, they are not on a sunset path, and using them alongside Router in the same integration is expected rather than a workaround. Reach for Router when you want one route shape and one credential across many models; reach for `/proxy/…` when you need a specific partner operation, a provider's own streaming response, or the submit-and-poll control that Router deliberately hides. + +**Status: deliberate.** Router narrows the surface on purpose — one route shape is the feature. The proxy surface stays where it is. + +## Next + +- [Comfy Router quickstart](/api-reference/comfy-router/quickstart) — a first working call in Python or TypeScript. +- [Comfy Router API reference](/api-reference/comfy-router/reference) — every endpoint, every parameter and every error bucket Router does send. diff --git a/api-reference/comfy-router/quickstart.mdx b/api-reference/comfy-router/quickstart.mdx new file mode 100644 index 000000000..07936f73f --- /dev/null +++ b/api-reference/comfy-router/quickstart.mdx @@ -0,0 +1,305 @@ +--- +title: "Comfy Router quickstart" +description: "From nothing to a generated image in about five minutes, in Python and TypeScript, against the Comfy Router." +--- + + +**Comfy Router is not generally available yet.** The routes below — +`POST /v1/models/{provider}/{model}` and its catalog and schema siblings — are +not serving requests yet: an authenticated call answers `404` today. This page +documents the contract they will serve, and is published ahead of that rollout so +the integration is ready to write against. It is not a description of behaviour +you can exercise right now. + + +Comfy Router runs partner models behind one host, one credential and one route shape. This page is the shortest complete path to a generated image: install a client, set a key, send one request, read the result — and see what the first failure looks like before you hit it. + +Base URL: `https://api.comfy.org`. The route is `POST /v1/models/{provider}/{model}`, the request body is the model's own native JSON input, and a `200` carries the model's own native JSON output. Router does not wrap either, so a call you already have written against the partner's API becomes a Router call by changing the host. + +## Why this page uses `bfl/flux-2-pro` + +`bfl/flux-2-pro` returns in about 3.1s at p50, which is the fastest measured path on the Router and is what makes a five-minute first result realistic — a slower model would spend that budget waiting rather than reading. + +It is a convenience, not a requirement. Every other model on the Router is called exactly the same way: same route, same credential header, same error buckets, same `X-Comfy-Request-Id`. Only the model ID, the fields inside the request body, and the shape of the result you read back change. Gemini, for instance, clears comfortably at 72.8s p95 — Router holds the connection for the whole generation rather than returning a job handle to poll. There is no edge ceiling cutting a long call short, but Router does bound the call itself: its own server deadline (10 minutes by default) is the longest it will hold a connection, after which it answers `504` / `deadline_exceeded` and does not bill. Swap the ID and read that model's fields from its own schema (below). + +## Get a key + +Router authenticates with a Comfy API key. Create one at [platform.comfy.org/profile/api-keys](https://platform.comfy.org/profile/api-keys), then put it in the environment — both samples below read `COMFY_API_KEY` and neither takes a key as a literal, so a copy-pasted snippet cannot carry your credential into a commit. + +```bash +export COMFY_API_KEY="comfyui-..." +``` + + +Send a `comfyui-` key in the **`X-API-Key`** header, not `Authorization: Bearer`. +The two headers select different validators: `X-API-Key` is the only inbound +reader of a `comfyui-` key, while a value in `Authorization` is routed to the JWT +branch, where a non-JWT token is a terminal `401 Invalid token` and the key is +never looked up. (`Authorization: Bearer` is correct for a Cloud/Firebase **JWT** +— that is what the generated +[API reference](/api-reference/comfy-router/reference) means by "bearer token".) + + +Keys are per workspace and carry that workspace's model entitlements and credit balance. A request with no usable credential comes back `401` with `X-Comfy-Error-Type: unauthorized`; one whose workspace cannot run the model comes back `403` / `forbidden`. + +## cURL + +The shortest possible call, for scripts, smoke tests and copy-paste into a terminal: + +```bash +curl https://api.comfy.org/v1/models/bfl/flux-2-pro \ + -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"}' +``` + +The response is the model's native output, exactly as the samples below read it. On failure, the body carries the error and the `X-Comfy-Error-Type` header names the bucket; keep the `X-Comfy-Request-Id` header from any response you need to ask about later. macOS and Linux both ship `uuidgen`; on Windows, generate the Idempotency-Key with `New-Guid` or any UUID source. + +## Python + +Requires Python 3.9+ and `httpx`: + +```bash +pip install httpx +``` + +Save as `quickstart.py` and run it with `python quickstart.py`: + +```python +import os +import uuid + +import httpx + +BASE_URL = os.environ.get("COMFY_ROUTER_BASE_URL", "https://api.comfy.org") +MODEL = "bfl/flux-2-pro" + +# Give the client headroom ABOVE Router's own server deadline (10 minutes by +# default) so a call that reaches the server bound comes back as a typed 504 +# with a request id rather than as an opaque client abort. The deadline bounds +# how long Router holds the connection, not whether the call is billed: if the +# provider completed the generation, it is billed either way. +READ_TIMEOUT_SECONDS = 660.0 + + +class RouterError(Exception): + """A Comfy Router failure, typed by its X-Comfy-Error-Type bucket.""" + + def __init__(self, response: httpx.Response) -> None: + self.error_type = response.headers.get("X-Comfy-Error-Type", "internal_error") + self.request_id = response.headers.get("X-Comfy-Request-Id") + self.status_code = response.status_code + # Parse defensively: an error can arrive as an HTML 502 from a load + # balancer, a plain-text 429, an empty body or a truncated JSON one. The + # status, the bucket and the request id above are the parts worth + # keeping, so a body that will not parse must not replace this exception + # with a JSONDecodeError and lose them. + body = None + if response.headers.get("content-type", "").startswith("application/json"): + try: + body = response.json() + except ValueError: + body = None + detail = body.get("detail") if isinstance(body, dict) else None + # A 422 carries a detail[] array - one entry per rejected field, each + # keeping its own `loc`, `msg` and `type`. Every other bucket carries a + # plain `detail` string. + self.errors = detail if isinstance(detail, list) else [] + self.detail = detail if isinstance(detail, str) else f"HTTP {response.status_code}" + super().__init__(self.detail) + + +def run(model: str, arguments: dict, idempotency_key: str) -> dict: + # Idempotency-Key makes a retry safe on a PAID call: Router replays the + # original response for 24h instead of dispatching (and billing) the + # provider a second time. Reuse the SAME key when retrying one logical + # call; generate a new one for a new call. + response = httpx.post( + f"{BASE_URL}/v1/models/{model}", + headers={ + "X-API-Key": os.environ["COMFY_API_KEY"], + "Idempotency-Key": idempotency_key, + }, + json=arguments, + timeout=httpx.Timeout(READ_TIMEOUT_SECONDS, connect=10.0), + ) + if response.is_error: + raise RouterError(response) + return response.json() + + +result = run( + MODEL, + {"prompt": "a red teapot on a windowsill, morning light"}, + idempotency_key=str(uuid.uuid4()), +) +# Router forwards each provider's native output unchanged, so this path is +# BFL's, not a Router envelope. Reading a different model means reading its own +# output shape. +print("image:", result["result"]["sample"]) + +# The first failure most callers hit: a field the model's input schema requires +# is missing, so Router rejects the request BEFORE any provider call - which is +# why a 422 is never billed. +try: + run(MODEL, {"width": 1024}, idempotency_key=str(uuid.uuid4())) +except RouterError as exc: + print(f"{exc.error_type} (HTTP {exc.status_code}), request id {exc.request_id}") + for entry in exc.errors: + print(" ", ".".join(str(p) for p in entry["loc"]), "->", entry["msg"]) +``` + +```text +image: https://.../out.jpeg +invalid_input (HTTP 422), request id 6f1c... + body.prompt -> Field required +``` + +## TypeScript + +Requires Node 18+ (for built-in `fetch`, `AbortSignal.timeout` and `crypto.randomUUID`) and `tsx` to run TypeScript directly: + +```bash +npm install --save-dev tsx +``` + +Save as `quickstart.mts` — the `.mts` extension is load-bearing, because the file uses top-level `await` and that needs an ES module — and run it with `npx tsx quickstart.mts`: + +```typescript +const BASE_URL = process.env.COMFY_ROUTER_BASE_URL ?? "https://api.comfy.org"; +const MODEL = "bfl/flux-2-pro"; + +// Headroom ABOVE Router's own server deadline (10 minutes by default), so a +// call that reaches the server bound returns a typed 504 with a request id +// rather than aborting locally at the same moment. The deadline bounds how +// long Router holds the connection, not whether the call is billed: if the +// provider completed the generation, it is billed either way. +const CLIENT_TIMEOUT_MS = 660_000; + +interface ValidationEntry { + loc: (string | number)[]; + msg: string; + type: string; +} + +/** A Comfy Router failure, typed by its `X-Comfy-Error-Type` bucket. */ +class RouterError extends Error { + readonly errorType: string; + readonly requestId: string | null; + readonly status: number; + /** A 422 carries a `detail[]` array — one entry per rejected field, each + * keeping its own `loc`, `msg` and `type`. Every other bucket carries a + * plain `detail` string. */ + readonly errors: ValidationEntry[]; + + constructor(response: Response, body: unknown) { + const detail = + typeof body === "object" && body !== null + ? (body as { detail?: unknown }).detail + : undefined; + super(typeof detail === "string" ? detail : `HTTP ${String(response.status)}`); + this.name = "RouterError"; + this.errorType = response.headers.get("X-Comfy-Error-Type") ?? "internal_error"; + this.requestId = response.headers.get("X-Comfy-Request-Id"); + this.status = response.status; + this.errors = Array.isArray(detail) ? (detail as ValidationEntry[]) : []; + } +} + +/** Read a body without letting a non-JSON error page mask the real failure. */ +async function parseBody(response: Response): Promise { + const text = await response.text(); + try { + return JSON.parse(text) as unknown; + } catch { + return undefined; + } +} + +async function run( + model: string, + args: Record, + idempotencyKey: string, +): Promise { + // Idempotency-Key makes a retry safe on a PAID call: Router replays the + // original response for 24h instead of dispatching (and billing) the provider + // a second time. Reuse the SAME key when retrying one logical call. + const response = await fetch(`${BASE_URL}/v1/models/${model}`, { + method: "POST", + headers: { + "X-API-Key": process.env.COMFY_API_KEY ?? "", + "Idempotency-Key": idempotencyKey, + "Content-Type": "application/json", + }, + body: JSON.stringify(args), + signal: AbortSignal.timeout(CLIENT_TIMEOUT_MS), + }); + // Branch on `ok` FIRST: an HTML 502, a plain-text 429 or an empty body must + // still surface the status, the bucket and the request id. + const body = await parseBody(response); + if (!response.ok) throw new RouterError(response, body); + return body as T; +} + +const result = await run<{ result: { sample: string } }>( + MODEL, + { prompt: "a red teapot on a windowsill, morning light" }, + crypto.randomUUID(), +); +// Router forwards each provider's native output unchanged, so this path is +// BFL's, not a Router envelope. Reading a different model means reading its own +// output shape. +console.log("image:", result.result.sample); + +// The first failure most callers hit: a field the model's input schema requires +// is missing, so Router rejects the request BEFORE any provider call - which is +// why a 422 is never billed. +try { + await run(MODEL, { width: 1024 }, crypto.randomUUID()); +} catch (exc) { + if (!(exc instanceof RouterError)) throw exc; + console.log(`${exc.errorType} (HTTP ${String(exc.status)}), request id ${String(exc.requestId)}`); + for (const entry of exc.errors) console.log(" ", entry.loc.join("."), "->", entry.msg); +} +``` + +```text +image: https://.../out.jpeg +invalid_input (HTTP 422), request id 6f1c... + body.prompt -> Field required +``` + +## Reading the `422` + +The `422` is the one error worth understanding before your first real call, because it is the one you cause. It means Router checked your body against the model's own input schema and rejected it — a required field missing, a value outside a bound, an image too small. That check runs BEFORE any provider call, so a `422` costs nothing: no partner spend, no billing question to answer afterwards. It is not the same as a `400`, which is a request-level failure (a malformed cursor, an unreadable envelope) rather than a per-field one. + +Its body is the fal/FastAPI `detail[]` shape: an array with one entry per offending field, each keeping its own `loc` (the path to the field), `msg`, `type` (the specific, provider-level reason — `missing`, `value_error`, `image_too_small`) and, where the reason carries a bound, `ctx`. That per-field granularity is why the samples above keep the array as data instead of flattening it into the exception message. + + +A model whose input schema has not been authored yet resolves to a documented +permissive fallback that admits any JSON object, so it will forward a body +rather than answer `422`. The samples above show the shape you handle once a +schema exists; treat the `422` block as the error path, not as a guaranteed +response to that particular body. + + +That body carries no `error_type` field of its own, so on a `422` the `X-Comfy-Error-Type` header is the *only* machine-readable bucket. Both samples read the bucket from the header first for exactly that reason, which is also what makes one error class enough to cover every failure Router can return. + +`X-Comfy-Request-Id` is on every response — success, `4xx` and `5xx` alike — and is the id to quote in a support request. Both samples attach it to the exception rather than making you re-run with header logging on to find it. + +## Where the model's fields come from + +`prompt` is the only field `bfl/flux-2-pro` requires; `width`, `height`, `seed` and `output_format` are the ones you will reach for next. Rather than reproducing a field list that can drift, read the model's schema live: + +```bash +curl -H "X-API-Key: $COMFY_API_KEY" \ + https://api.comfy.org/v1/models/bfl/flux-2-pro/openapi.json +``` + +That is the same document the server validates your call against, served as a standalone OpenAPI document, so what is published and what is enforced cannot disagree. Take any model ID, append `/openapi.json` to its invocation path, and generate against what comes back. + +## Next + +- [Comfy Router API reference](/api-reference/comfy-router/reference) — every endpoint, every parameter, and all fifteen error buckets. +- [Comfy Router limitations](/api-reference/comfy-router/limitations) — what Router does not do today, and what to use instead. diff --git a/api-reference/comfy-router/reference.mdx b/api-reference/comfy-router/reference.mdx new file mode 100644 index 000000000..a9c0e250f --- /dev/null +++ b/api-reference/comfy-router/reference.mdx @@ -0,0 +1,300 @@ +--- +title: "Comfy Router API reference" +description: "Every Comfy Router endpoint, parameter, response body and error bucket, generated from the Comfy API contract." +--- + +{/* + GENERATED FILE -- DO NOT HAND-EDIT. + + Produced from the Comfy API contract by gen_router_reference.py. Edit the + contract and regenerate; an edit made here is overwritten by the next run and + is rejected by the drift gate in the meantime. +*/} + +Comfy Router's canonical, model-ID-addressed routes. + +Base URL: `https://api.comfy.org` + +Every endpoint below is authenticated. Send `Authorization: Bearer `. + +## Endpoints + +### `GET /v1/models` + +**List the models Comfy Router can run.** + +Comfy Router's model catalog - one page of the canonical model IDs that `POST /v1/models/{provider}/{model}` accepts. An SDK calls this on cold start to discover what is runnable, and the `model_not_found` suggestions come from the same catalog, so an ID listed here that then 404s on invocation would be worse than either failure alone. That agreement is structural rather than a promise: an entry's `provider` and `model` are the two path segments of the invocation route and reference the SAME schema components that route's path parameters do, and `id` is those two segments joined by `/`. + +**Parameters** + +| Name | In | Required | Type | Constraints | Description | +| --- | --- | --- | --- | --- | --- | +| `cursor` | query | no | [`RouterPageCursor`](#routerpagecursor) | `pattern: ^[A-Za-z0-9._~+/=-]+$`, `minLength: 1`, `maxLength: 512` | Opaque pagination cursor. Pass a previous page's `next_cursor` to fetch the next page; omit it for the first page. See `RouterPageCursor` for why the value is opaque and why this route paginates by cursor rather than by offset. | +| `limit` | query | no | integer | `maximum: 100`, `default: 20` | Number of models to return in one page. Values above the declared maximum are outside the contract, but this route does not reject them: it serves the maximum instead, and the page size actually served is echoed back as `limit` on the response, so a clamp is always detectable by the caller. Treat the maximum as the real page stride - a client that asks for more and assumes it received more will miss rows. 0 and negative values are also accepted and select the default, which is why no `minimum` is declared: sub-1 is meaningful here, not invalid. | + +**Responses** + +| Status | Body | Headers | Description | +| --- | --- | --- | --- | +| `200` | [`RouterModelListResponse`](#routermodellistresponse) | `X-Comfy-Request-Id` | OK - one page of the model catalog. | +| `400` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`. | +| `401` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`. | + +### `GET /v1/models/{provider}/{model}` + +**Read one partner model's catalog entry by canonical model ID.** + +Per-model detail for a single Comfy Router model, so a caller can check one model without walking the whole paginated catalog. The SDKs use it to look a model up immediately before invoking it. + +**Parameters** + +| Name | In | Required | Type | Constraints | Description | +| --- | --- | --- | --- | --- | --- | +| `provider` | path | yes | [`RouterProviderSegment`](#routerprovidersegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 64` | Lowercase provider segment of the canonical `{provider}/{model}[/{variant}]` model ID - the partner whose model is being run. | +| `model` | path | yes | [`RouterModelSegment`](#routermodelsegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 128` | Lowercase model segment of the canonical `{provider}/{model}[/{variant}]` model ID - the model to run within that provider. | + +**Responses** + +| Status | Body | Headers | Description | +| --- | --- | --- | --- | +| `200` | [`RouterModelDetail`](#routermodeldetail) | `X-Comfy-Request-Id` | OK - the model's catalog entry. | +| `404` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`. | + +### `POST /v1/models/{provider}/{model}` + +**Run a partner model synchronously by canonical model ID.** + +Comfy Router's canonical, model-ID-addressed entry point. The request body is the partner model's OWN native JSON input and the success response is that model's OWN native JSON output: Router forwards both unchanged instead of imposing a Comfy-shaped envelope, so a caller can move between the partner's API and Router by changing the host. This is the SYNCHRONOUS path, mirroring `POST https://fal.run/{id}` - the response carries the finished result. A queued counterpart, `/v1/queue/models/{provider}/{model}`, is planned and would put fal's `fal.run` / `queue.fal.run` split onto a single host; it is not part of this contract yet. + +**Parameters** + +| Name | In | Required | Type | Constraints | Description | +| --- | --- | --- | --- | --- | --- | +| `provider` | path | yes | [`RouterProviderSegment`](#routerprovidersegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 64` | Lowercase provider segment of the canonical `{provider}/{model}[/{variant}]` model ID - the partner whose model is being run. | +| `model` | path | yes | [`RouterModelSegment`](#routermodelsegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 128` | Lowercase model segment of the canonical `{provider}/{model}[/{variant}]` model ID - the model to run within that provider. | + +**Request body** + +`application/json` -- [`RouterModelInput`](#routermodelinput) (required) + +The partner model's native JSON input, forwarded to the provider unchanged. + +**Responses** + +| Status | Body | Headers | Description | +| --- | --- | --- | --- | +| `200` | [`RouterModelOutput`](#routermodeloutput) | `X-Comfy-Request-Id` | OK - the partner model's native JSON output, returned unchanged. | +| `403` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`. | +| `404` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`. | +| `422` | [`RouterValidationErrorResponse`](#routervalidationerrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | The request reached the model and the model rejected its contents. The body is `RouterValidationErrorResponse`, the fal/FastAPI `detail[]` shape, so each offending field keeps its own specific `type` and `ctx`. `X-Comfy-Error-Type` carries the coarse bucket for the whole response. | +| `503` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`. | +| `504` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`. | + +### `GET /v1/models/{provider}/{model}/openapi.json` + +**Read one partner model's input schema as an OpenAPI document.** + +The per-model input schema for a single Comfy Router model, served as a standalone OpenAPI document, so a caller - an SDK, a codegen tool, or an agent - can discover a model's arguments without reading Comfy's prose docs. It mirrors fal's per-model schema endpoint, and it is the discovery mechanism the SDK quickstart depends on. + +**Parameters** + +| Name | In | Required | Type | Constraints | Description | +| --- | --- | --- | --- | --- | --- | +| `provider` | path | yes | [`RouterProviderSegment`](#routerprovidersegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 64` | Lowercase provider segment of the canonical `{provider}/{model}[/{variant}]` model ID - the partner whose model is being run. | +| `model` | path | yes | [`RouterModelSegment`](#routermodelsegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 128` | Lowercase model segment of the canonical `{provider}/{model}[/{variant}]` model ID - the model to run within that provider. | + +**Responses** + +| Status | Body | Headers | Description | +| --- | --- | --- | --- | +| `200` | [`RouterModelInputSchemaDocument`](#routermodelinputschemadocument) | `X-Comfy-Request-Id`, `ETag`, `Cache-Control` | OK - the model's input schema, as a standalone OpenAPI document. | +| `304` | - | `X-Comfy-Request-Id`, `ETag`, `Cache-Control` | Not Modified - the document is unchanged since the `ETag` the caller sent in `If-None-Match`. No body is returned. | +| `404` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`. | +| `500` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`. | + +## Error buckets + +Coarse, machine-readable bucket for a Router failure, mirrored on the `X-Comfy-Error-Type` response header so a caller can branch without parsing the body. The set is closed at fifteen values: the six request-level buckets `invalid_input`, `content_policy_violation`, `provider_error`, `provider_timeout`, `insufficient_credits` and `model_not_found`, plus the transport-level `unauthorized`, `forbidden`, `concurrency_limit_exceeded`, `client_disconnected`, `internal_error`, `deadline_exceeded`, `not_enabled`, `service_unavailable` and `rate_limited`. + +### Request-level buckets + +Raised for a request Router accepted and then could not complete. + +| `error_type` | Meaning | +| --- | --- | +| `invalid_input` | The request was rejected before it reached the model - a malformed body, a malformed or expired pagination cursor, or an input the model's own schema does not accept. | +| `content_policy_violation` | The provider refused the request on content-policy grounds. The refusal is deterministic: re-sending the same input will be refused again. | +| `provider_error` | The partner provider reported a failure of its own, or returned a response Router could not interpret as a result. | +| `provider_timeout` | The partner provider did not answer within its deadline. This bucket is the PROVIDER timing out and never Router's own server deadline, which is reported as `deadline_exceeded` - the two share `504` and are separated because they name different causes: this one says the partner failed, that one says Comfy stopped holding the connection. | +| `insufficient_credits` | The calling workspace does not have enough credits to run the model. | +| `model_not_found` | The `{provider}/{model}` ID names no model Router can run; an unknown provider lands here too. `detail` carries up to three suggestions drawn from the models the caller is entitled to see. | + +### Transport-level buckets + +Raised by Router itself, before or around the call to the model. + +| `error_type` | Meaning | +| --- | --- | +| `unauthorized` | The request carried no usable credential. | +| `forbidden` | The credential is valid but is not entitled to this model or this operation. | +| `concurrency_limit_exceeded` | The workspace already has as many calls in flight as it is allowed; retry once one of them finishes. | +| `client_disconnected` | The caller closed the connection before Router could return a result. It is logged rather than delivered - there is no socket left to write it to - and it is an attribution, not a billing outcome: a provider generation that completed is billed regardless of whether the caller received the response. | +| `internal_error` | Router itself failed. It is also the value a client should treat any UNRECOGNIZED bucket as, so a later addition to the set does not break a client generated before it. | +| `deadline_exceeded` | Comfy stopped holding the connection at its own configured bound before an answer arrived. It shares `504` with `provider_timeout` and the pair says which side ran out of time; this one is Comfy's own bound, so nothing about the request was rejected and the same request may be retried. It says nothing about the charge: a provider generation that completed is billed regardless of whether the caller received the response. | +| `not_enabled` | Comfy Router is not switched on for this caller yet. Nothing about the request is wrong and the model exists, which is why this is not `model_not_found`; it shares `403` with `forbidden` and is NOT the same thing, because `forbidden` is an entitlement decision about the caller while this is a state of the rollout. It is TERMINAL: do not retry, and do not treat it as an outage. | +| `service_unavailable` | A service Comfy Router depends on is temporarily unavailable and the caller did nothing wrong. Retry it with backoff: it is the one bucket here whose condition clears on its own, without the caller changing the request and without a concurrency slot freeing, which is what distinguishes it from the other retryable answers (`concurrency_limit_exceeded`, `deadline_exceeded`). It is separate from `internal_error` - which is a `500` and means Router itself failed - so a client can tell "come back shortly" from "this call is not going to work". | +| `rate_limited` | The caller has spent an allowance measured over a WINDOW and must wait for that window to roll. It shares `429` with `concurrency_limit_exceeded` and is not the same thing: that one clears the moment one of the caller's own in-flight calls finishes, so retrying in seconds is right, whereas nothing the caller does drains this one early. `detail` names the window. | + +## Response headers + +| Header | Type | Description | +| --- | --- | --- | +| `Cache-Control` | string | Freshness directives for the served schema document. `private` because the route is authenticated - the document itself is not caller-specific, but a shared cache must not hold a response to an authenticated request - and `must-revalidate` so a stale copy is revalidated against the `ETag` rather than served on. | +| `ETag` | string | Strong entity tag over the served document's bytes, for `GET /v1/models/{provider}/{model}/openapi.json`. A per-model schema changes rarely and an SDK re-fetches it often, so a caller should store this value and send it back as `If-None-Match` to get a `304` instead of the document. | +| `X-Comfy-Error-Type` | [`RouterErrorType`](#routererrortype) | Coarse, machine-readable bucket for the failure, set by Router on every error response. It carries the same value as `RouterErrorResponse.error_type`, and on the `422` it is the ONLY machine-readable bucket, because that body is the fal/FastAPI `detail[]` shape and has no `error_type` field of its own. A client can therefore branch on this header alone, before deciding which of the two Router error bodies it received. | +| `X-Comfy-Request-Id` | string | Server-generated identifier for this call, present on EVERY Router response - success, 4xx and 5xx alike, because an error response is exactly when a user needs an id to quote in a support request. The SAME value is written into the call's usage/audit event, which is what lets a complaint about a charge be joined to the charge itself instead of searched for by timestamp. | + +## Per-model input schemas + +A model's own input fields are not reproduced here. Read them live from `GET /v1/models/{provider}/{model}/openapi.json`, which serves the same document the server validates the call against, so what is published and what is enforced cannot drift apart. Take a model ID from `GET /v1/models`, append `/openapi.json` to its invocation path, and generate against the document you get back. + +## Schemas + +### RouterChargesOnPolicyRejection + +Whether a call this model REFUSES on content-policy grounds is nevertheless charged to the caller. Providers differ, the difference is invisible at call time, and a user who sees an error and a charge for the same call has no way to have known - so it is stated per model, before the call, rather than left to per-provider folklore. + +Type: `string` + +### RouterErrorResponse + +Router's request-level error body: what is returned when the request never reached the model, or failed for a reason the model itself did not report - auth, quota, an unknown model ID, or provider transport. A model-level validation failure has its own shape, `RouterValidationErrorResponse`, because flattening a FastAPI `detail[]` array into this `detail` string would destroy the per-field granularity an SDK branches on. + +| Field | Type | Required | Constraints | Description | +| --- | --- | --- | --- | --- | +| `detail` | string | yes | - | Human-readable description of the failure, safe to surface to an end user. Not machine-parsed - branch on `error_type` instead. | +| `error_type` | [`RouterErrorType`](#routererrortype) | yes | - | Coarse, machine-readable bucket for a Router failure, mirrored on the `X-Comfy-Error-Type` response header so a caller can branch without parsing the body. The set is closed at fifteen values: the six request-level buckets `invalid_input`, `content_policy_violation`, `provider_error`, `provider_timeout`, `insufficient_credits` and `model_not_found`, plus the transport-level `unauthorized`, `forbidden`, `concurrency_limit_exceeded`, `client_disconnected`, `internal_error`, `deadline_exceeded`, `not_enabled`, `service_unavailable` and `rate_limited`. | + +### RouterErrorType + +Coarse, machine-readable bucket for a Router failure, mirrored on the `X-Comfy-Error-Type` response header so a caller can branch without parsing the body. The set is closed at fifteen values: the six request-level buckets `invalid_input`, `content_policy_violation`, `provider_error`, `provider_timeout`, `insufficient_credits` and `model_not_found`, plus the transport-level `unauthorized`, `forbidden`, `concurrency_limit_exceeded`, `client_disconnected`, `internal_error`, `deadline_exceeded`, `not_enabled`, `service_unavailable` and `rate_limited`. + +Type: `string` + +### RouterModelBilling + +Per-model billing FACTS a caller needs before invoking - not prices. Usage and cost figures never appear here. + +| Field | Type | Required | Constraints | Description | +| --- | --- | --- | --- | --- | +| `charges_on_policy_rejection` | [`RouterChargesOnPolicyRejection`](#routerchargesonpolicyrejection) | yes | - | Whether a call this model REFUSES on content-policy grounds is nevertheless charged to the caller. Providers differ, the difference is invisible at call time, and a user who sees an error and a charge for the same call has no way to have known - so it is stated per model, before the call, rather than left to per-provider folklore. | + +### RouterModelDetail + +Per-model detail for one Comfy Router model: everything the catalog listing reports for it, plus the per-model fields that only the single-model route carries. + +Composes [`RouterModelListEntry`](#routermodellistentry), [`RouterModelDetailFields`](#routermodeldetailfields). + +Type: `object` + +### RouterModelDetailFields + +The half of `RouterModelDetail` the catalog listing does NOT carry: per-model fields worth one lookup but not worth repeating on every entry of a paginated catalog page. + +| Field | Type | Required | Constraints | Description | +| --- | --- | --- | --- | --- | +| `input_schema_url` | string | no | `format: uri`, `pattern: ^https://`, `maxLength: 2048` | Pointer to this model's input schema document - the description of the body `POST /v1/models/{provider}/{model}` accepts for this model. Only the POINTER is part of this contract: the document it addresses is authored separately. Absent when no schema has been authored for the model. | + +### RouterModelId + +A canonical Comfy Router model ID, `{provider}/{model}` - exactly the value that addresses the model on `POST /v1/models/{provider}/{model}`, so a caller can interpolate it into that path without re-deriving it from anything. Its `pattern` is `RouterProviderSegment` and `RouterModelSegment` joined by a single `/`, and `maxLength` is their sum plus that separator. + +Type: `string` -- `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*/[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 193` + +### RouterModelInput + +A partner model's native JSON input document, forwarded to the provider as-is. Its concrete shape is owned by the partner rather than by Comfy, so this is an open object: Router does not narrow, rename, or re-envelope the fields. It is a named component (never an inline anonymous object) because ComfyUI's spec-driven codegen needs a class to generate. + +Type: `object` + +### RouterModelInputSchemaDocument + +A standalone OpenAPI document describing ONE Comfy Router model's input - the body `POST /v1/models/{provider}/{model}` accepts for that model. It is what `GET /v1/models/{provider}/{model}/openapi.json` returns. + +Type: `object` + +### RouterModelListEntry + +One entry in the Router model catalog: the identity of a runnable model, and nothing else. The per-model detail route composes this same entry rather than restating it, which is why the name is `...ListEntry` and not `...Summary` - there must be exactly one definition of what a catalog entry is. Per-model detail and the per-model input/output schemas are their own routes, so this shape stays the minimum a caller needs in order to invoke the model - deliberately, because this is the payload an SDK fetches on cold start. `id` is `provider` and `model` joined by `/`; the two fields are carried separately as well so a caller composes the invocation path without splitting a string. + +| Field | Type | Required | Constraints | Description | +| --- | --- | --- | --- | --- | +| `id` | [`RouterModelId`](#routermodelid) | yes | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*/[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 193` | A canonical Comfy Router model ID, `{provider}/{model}` - exactly the value that addresses the model on `POST /v1/models/{provider}/{model}`, so a caller can interpolate it into that path without re-deriving it from anything. Its `pattern` is `RouterProviderSegment` and `RouterModelSegment` joined by a single `/`, and `maxLength` is their sum plus that separator. | +| `provider` | [`RouterProviderSegment`](#routerprovidersegment) | yes | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 64` | Lowercase `provider` segment of the canonical `{provider}/{model}[/{variant}]` model ID - the partner whose model is being addressed. The invocation route's `provider` path parameter and a catalog entry's `provider` field both reference this one schema, which is what keeps the listed IDs and the accepted IDs from drifting apart. | +| `model` | [`RouterModelSegment`](#routermodelsegment) | yes | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 128` | Lowercase `model` segment of the canonical `{provider}/{model}[/{variant}]` model ID - the model to run within that provider. Shared by the invocation route's `model` path parameter and a catalog entry's `model` field, for the same no-drift reason as `RouterProviderSegment`. | +| `billing` | [`RouterModelBilling`](#routermodelbilling) | yes | - | Per-model billing FACTS a caller needs before invoking - not prices. Usage and cost figures never appear here. | + +### RouterModelListResponse + +One page of the Router model catalog. + +| Field | Type | Required | Constraints | Description | +| --- | --- | --- | --- | --- | +| `data` | array of [`RouterModelListEntry`](#routermodellistentry) | yes | - | The models on this page, at most `limit` of them. | +| `has_more` | boolean | yes | - | Whether another page exists beyond this one. Keep walking while this is true; do not infer the end of the catalog from a short or empty `data`. | +| `next_cursor` | [`RouterPageCursor`](#routerpagecursor) | no | `pattern: ^[A-Za-z0-9._~+/=-]+$`, `minLength: 1`, `maxLength: 512` | An OPAQUE cursor into a Router list. It is produced by the server and only ever round-tripped: it is not an offset, not a model ID, not ordered, and not stable across catalog rebuilds, so parsing one, incrementing one, or persisting one beyond the walk it came from are all outside the contract. Cursor rather than offset because the catalog is a moving list - an offset walk silently skips or repeats entries when entries are added or removed mid-walk, and a caller cannot tell that it happened. | +| `limit` | integer | yes | `minimum: 1`, `maximum: 100` | The page size actually served. A requested `limit` above the maximum is CLAMPED down to the maximum rather than rejected, so this can be smaller than the value asked for - paginate with this number, not with the one you sent, or you will assume rows you never received. | + +### RouterModelOutput + +A partner model's native JSON output document, returned to the caller as-is. Its concrete shape is owned by the partner rather than by Comfy, so this is an open object: Router does not narrow, rename, or re-envelope the fields. It is a named component (never an inline anonymous object) because ComfyUI's spec-driven codegen needs a class to generate. + +Type: `object` + +### RouterModelSegment + +Lowercase `model` segment of the canonical `{provider}/{model}[/{variant}]` model ID - the model to run within that provider. Shared by the invocation route's `model` path parameter and a catalog entry's `model` field, for the same no-drift reason as `RouterProviderSegment`. + +Type: `string` -- `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 128` + +### RouterPageCursor + +An OPAQUE cursor into a Router list. It is produced by the server and only ever round-tripped: it is not an offset, not a model ID, not ordered, and not stable across catalog rebuilds, so parsing one, incrementing one, or persisting one beyond the walk it came from are all outside the contract. Cursor rather than offset because the catalog is a moving list - an offset walk silently skips or repeats entries when entries are added or removed mid-walk, and a caller cannot tell that it happened. + +Type: `string` -- `pattern: ^[A-Za-z0-9._~+/=-]+$`, `minLength: 1`, `maxLength: 512` + +### RouterProviderSegment + +Lowercase `provider` segment of the canonical `{provider}/{model}[/{variant}]` model ID - the partner whose model is being addressed. The invocation route's `provider` path parameter and a catalog entry's `provider` field both reference this one schema, which is what keeps the listed IDs and the accepted IDs from drifting apart. + +Type: `string` -- `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 64` + +### RouterValidationErrorContext + +The violated bound for one `RouterValidationErrorDetail`, carried from the provider verbatim - for example `{"limit_value": 8}` alongside `greater_than`, `{"min_width": 512}` alongside `image_too_small`, or `{"max_size_bytes": 10485760}` alongside `file_too_large`. The key set is specific to the provider and the error type, so this is deliberately an open object: narrowing it to a fixed field list, or folding it into the `msg` string, is precisely how a ported integration compiles and then silently loses the branch that read the bound. Absent when the error type carries no bound. + +Type: `object` + +### RouterValidationErrorDetail + +One model-level validation failure, in the fal/FastAPI form. `type` carries the SPECIFIC provider reason - `value_error`, `missing`, `image_too_small`, `unsupported_audio_format`, `greater_than`, `file_too_large` and the rest - which is the granularity `RouterErrorType`'s coarse bucket cannot express. It is an open string and not an `enum` for the same reason: the provider vocabulary runs to roughly 48 values across two tiers and grows on the provider's release cycle, not ours, and an unmodelled value must reach the caller rather than fail deserialization. + +| Field | Type | Required | Constraints | Description | +| --- | --- | --- | --- | --- | +| `loc` | array of any | yes | - | Path to the offending field, outermost segment first - for example `["body", "image_url"]`, or `["body", "images", 0]` where an integer indexes into an array. | +| `msg` | string | yes | - | Human-readable description of this single failure. | +| `type` | string | yes | - | Specific, machine-readable reason for this failure, passed through from the provider unchanged. This is the value a typed SDK exception hierarchy branches on; `error_type` on the response header is only its coarse bucket. | +| `ctx` | [`RouterValidationErrorContext`](#routervalidationerrorcontext) | no | - | The violated bound for one `RouterValidationErrorDetail`, carried from the provider verbatim - for example `{"limit_value": 8}` alongside `greater_than`, `{"min_width": 512}` alongside `image_too_small`, or `{"max_size_bytes": 10485760}` alongside `file_too_large`. The key set is specific to the provider and the error type, so this is deliberately an open object: narrowing it to a fixed field list, or folding it into the `msg` string, is precisely how a ported integration compiles and then silently loses the branch that read the bound. Absent when the error type carries no bound. | +| `input` | [`RouterValidationErrorInput`](#routervalidationerrorinput) | no | - | The offending input value, echoed back verbatim so a caller can see what was rejected without re-deriving it from `loc`. Any JSON type - string, number, boolean, array, object or null - so this schema is deliberately left untyped rather than narrowed to an object. Absent when the provider does not echo the input back. | + +### RouterValidationErrorInput + +The offending input value, echoed back verbatim so a caller can see what was rejected without re-deriving it from `loc`. Any JSON type - string, number, boolean, array, object or null - so this schema is deliberately left untyped rather than narrowed to an object. Absent when the provider does not echo the input back. + +### RouterValidationErrorResponse + +Router's model-level `422` body, in the fal/FastAPI form: the request was well-formed enough to reach the model and the model rejected its contents. Note it carries no `error_type` of its own - that is what `X-Comfy-Error-Type` on the response is for, so a client can read the coarse bucket off the header without first deciding which of the two Router error bodies it received. + +| Field | Type | Required | Constraints | Description | +| --- | --- | --- | --- | --- | +| `detail` | array of [`RouterValidationErrorDetail`](#routervalidationerrordetail) | yes | - | Every validation failure found on the request, one entry per offending field. | diff --git a/api-reference/v2/overview.mdx b/api-reference/v2/overview.mdx index 370c1d3d4..13885bade 100644 --- a/api-reference/v2/overview.mdx +++ b/api-reference/v2/overview.mdx @@ -34,3 +34,7 @@ See [Design Notes](/development/api-development/sdks-design) for the reasoning b |----------|-------------| | Assets | UUID-identified records over content-addressed blobs. Upload inputs, download outputs. | | Jobs | One execution of a workflow. Durable, pollable, and cancelable. | + +## Comfy Router + +Comfy API v2 runs workflows as durable jobs you submit and poll. For direct model calls (one partner model, one request, the model's native input and output), see the [Comfy Router](/api-reference/comfy-router/quickstart) instead. diff --git a/docs.json b/docs.json index d02298396..c7dc72f30 100644 --- a/docs.json +++ b/docs.json @@ -2805,6 +2805,14 @@ "development/comfyui-server/api-key-integration" ] }, + { + "group": "Comfy Router", + "pages": [ + "api-reference/comfy-router/quickstart", + "api-reference/comfy-router/reference", + "api-reference/comfy-router/limitations" + ] + }, { "group": "Comfy CLI", "pages": [ @@ -5768,6 +5776,14 @@ "zh/development/comfyui-server/api-key-integration" ] }, + { + "group": "Comfy Router", + "pages": [ + "zh/api-reference/comfy-router/quickstart", + "zh/api-reference/comfy-router/reference", + "zh/api-reference/comfy-router/limitations" + ] + }, { "group": "Comfy CLI", "pages": [ @@ -8731,6 +8747,14 @@ "ja/development/comfyui-server/api-key-integration" ] }, + { + "group": "Comfy Router", + "pages": [ + "ja/api-reference/comfy-router/quickstart", + "ja/api-reference/comfy-router/reference", + "ja/api-reference/comfy-router/limitations" + ] + }, { "group": "Comfy CLI", "pages": [ @@ -11672,6 +11696,14 @@ "ko/development/comfyui-server/api-key-integration" ] }, + { + "group": "Comfy Router", + "pages": [ + "ko/api-reference/comfy-router/quickstart", + "ko/api-reference/comfy-router/reference", + "ko/api-reference/comfy-router/limitations" + ] + }, { "group": "Comfy CLI", "pages": [ diff --git a/ja/api-reference/comfy-router/limitations.mdx b/ja/api-reference/comfy-router/limitations.mdx new file mode 100644 index 000000000..c1abfa65e --- /dev/null +++ b/ja/api-reference/comfy-router/limitations.mdx @@ -0,0 +1,113 @@ +--- +title: "Comfy Router の制限事項" +description: "Comfy Router が現在できないこと、代替手段が存在する場合に代わりに使用すべきもの、およびこれらの制限のうち変更される見込みのあるものについて説明します。" +translationSourceHash: d49d86b7 +translationFrom: api-reference/comfy-router/limitations.mdx +translationBlockHashes: + "_intro": c72d3766 + "At a glance": dd3cde31 + "No queued submission": f24e2b9d + "No cost or credit figures on a response": 0d8505a9 + "No way to resume a call you lost": 99da3819 + "Calls are cut off at a server deadline": 29b27f1c + "No progress while a call runs": 40667680 + "Three forecast buckets are not in the vocabulary": 6f256b4e + "Router does not cover every partner operation": 069b148a + "Next": c839d9e8 +--- + +**Comfy Router はまだ一般提供されていません。** 以下で参照されるルート(`POST /v1/models/{provider}/{model}` と、そのカタログおよびスキーマ関連ルート)は、まだリクエストを処理していません。現時点では、認証済みの呼び出しでも `404` が返ります。このページは、それらのルートが提供する予定の契約について説明したものであり、既知の形状に対して統合を記述できるよう、その展開に先立って公開されています。以下の内容はすべて、その契約に関する記述であり、現在実際に試すことができる動作に関するものではありません。 + + +Comfy Router は1回の同期呼び出しです。パートナーモデルのネイティブな入力を、1つの認証情報で1つのホストに送信し、接続は開いたままになり、`200` がそのモデルのネイティブな出力を運びます。この形状こそが最初の統合を短くするものであり、また、このページに記載されたすべての制限の由来でもあります。Router を中心に設計する前に、このページを読んでください。後ではなく前です。以下に続く内容のほとんどには単純明快な代替手段があり、代替手段のないものは、Router が保持しない前提に基づいて構築する前に知っておく価値があります。 + +## 概要 + +各行は、その制限を説明するセクションへのリンクになっています。**Deliberate(意図的)** は、その制限がRouterの動作の一部であり、今後の実装を待つものではないことを意味します。**Not yet(未対応)** は、Routerがこの機能を獲得する見込みがあることを意味しますが、このページは時期についての約束をするものではありません。 + +| 制限 | 代替手段 | ステータス | +| --- | --- | --- | +| [キュー中の送信はない: 呼び出しは同期](#キュー投入はありません) | 接続を開いたままにするか、送信とポーリングを行うパートナープロキシルートを使用してください | 未対応 | +| [レスポンスにコストやクレジットの情報がない](#no-cost-or-credit-figures-on-a-response) | Comfyプラットフォームで残高と使用量を確認してください。呼び出し前にモデルのカタログエントリの`billing`を確認してください | 未対応 | +| [失った呼び出しを再開する方法がない](#no-way-to-resume-a-call-you-lost) | `Idempotency-Key`を送信すると、リトライの課金が最大でも1回に抑えられます。ただし、失われた結果を回復することはできません | 未対応 | +| [サーバーの期限で呼び出しが切断される](#呼び出しはサーバーの期限で打ち切られる) | クライアントに期限を超えるタイムアウトを設定してください。期限内に完了できない作業は分割してください | 意図的 | +| [呼び出し実行中に進行状況がない](#呼び出し実行中は進捗がありません) | 現在Routerにはありません。パートナープロキシルートが独自の進行状況を公開する場合があります | 未対応 | +| [3つの予測バケットが語彙にない](#3つの予測バケットは語彙に含まれていない) | Routerが公開する15のバケットを処理してください。認識できないものはすべて`internal_error`として扱ってください | 未対応 | +| [Routerはすべてのパートナー操作をカバーしていない](#router-does-not-cover-every-partner-operation) | 同じホストの`/proxy/…`配下にあるパートナープロキシルートを使用してください | 意図的 | + +## キュー投入はありません + +モデルの実行方法は1つだけです。`POST /v1/models/{provider}/{model}` は、生成が完了するまで接続を保持し、レスポンスで結果を返します。ジョブを受け付けて識別子を返し、後で結果を取得できるようにするエンドポイントはなく、完了時のコールバックやウェブフックもありません。キュー投入に対応する同等のエンドポイントは計画されており、APIリファレンスでは `/v1/queue/models/{provider}/{model}` として記載されています。ただし、これは現時点では契約の一部ではなく、このエンドポイントへの呼び出しは処理されません。 + +**代わりにすべきこと。** ほとんどのモデルではこれは問題になりません。接続を開いたままにして結果を読み取ってください。高速な画像モデルは数秒で結果を返します。長時間のビデオ生成は数分かかることもありますが、Routerはその間接続を保持します。クライアントの読み取りタイムアウトは、[Router自身の期限](#呼び出しはサーバーの期限で打ち切られる) よりも長い余裕のある値に設定し、この呼び出しを高速なリクエストではなく長時間実行として扱ってください。アーキテクチャ上、どうしても接続を開いたままにできない場合(実行時間の上限が短いサーバーレス関数や、ユーザーが閉じることを想定しているブラウザタブなど)は、接続を保持できる自分が管理するワーカーから呼び出しを実行するか、独自の送信・ポーリングのペアを備えたプロバイダー向けのパートナープロキシルートを使用してください。[最後のセクション](#router-does-not-cover-every-partner-operation) を参照してください。 + +**ステータス: 未実装。** キュー投入のパスは予定されていますが、このページでは時期については何も約束していません。 + +## No cost or credit figures on a response + +A Router response tells you what the model produced, and its contract says nothing about what it cost. There is no charge amount, no credit balance and no usage figure in the body, and the route declares no cost header. One caveat, so it does not surprise you: Router shares a billing path with the partner-proxy routes, and that path stamps `X-Comfy-Credits-Used` on a billed response for an allowlist of providers, so the header can appear on a Router call to one of them. It is not part of Router's contract — it is absent for every provider outside that allowlist, and it is deliberately *not* replayed on an idempotent retry, precisely so a client summing it cannot double-count a call that was only paid for once. Do not build reconciliation on it. The model catalog is the same: it carries billing *facts* a caller needs before invoking, never prices. So you cannot reconcile spend from a Router response alone, and you cannot show a user "this call cost X" without getting X from somewhere else. + +**What to do instead.** Your balance, your usage and your invoices live on the Comfy platform at [platform.comfy.org](https://platform.comfy.org) — that is the source of truth for what you have spent and what you have left, and it is unaffected by anything on this page. Two things Router does tell you at call time are worth using: a call refused for lack of credit comes back as `insufficient_credits`, so you can handle exhaustion as a typed error rather than by pre-checking a balance; and each model's catalog entry carries `billing.charges_on_policy_rejection`, which says whether that specific model charges you for a generation it then refuses on content-policy grounds. It is a **string with three values**, not a boolean: `yes`, `no` and `unknown`. Read `unknown` as "this might charge you" — it means nobody has established that model's behaviour yet, and it exists precisely so an unchecked model is not published as a `no`, which is a claim. The field is deliberately not an `enum`, so treat any value you do not recognize as `unknown` too, and do not write a truthiness check over it: the string `"no"` is truthy in most languages, and that check gets backwards the one case it exists to catch. Providers differ on that, the difference is invisible at call time, and reading it before you call is how you avoid a charge you cannot explain afterwards. + +**Status: not yet** for per-call figures. Note that the *catalog* deliberately carries no prices — pricing belongs where pricing is maintained, not duplicated into a model listing that would drift from it. + +## No way to resume a call you lost + +Router does not keep a resumable record of an in-flight call. There is no status route, no job identifier, and nothing to reconnect to: if the connection drops mid-call — a client crash, a network partition, a deploy that restarts your process — the response is gone, and the call is not something you can ask about afterwards. Whether the *generation* completed and was charged is a separate question from whether you received it, and losing the connection does not reliably answer either. + +**What to do instead.** Send an `Idempotency-Key` header on every call. It does not make a lost call resumable, but it makes retrying one safe. Router reserves the key for the duration of the call, and when the call actually reached you with an answer it records that response against the key for 24 hours; retrying with the **same** key then replays the recorded response instead of dispatching — and re-charging — the provider a second time, marked `Idempotent-Replayed: true` so you can tell a replay from a fresh run. Generate a fresh key per logical call, not per attempt; the same key presented with a *different* request body is a `409` rather than a silent overwrite. + +Be precise about what that buys you, because it is a **billing** property and not a delivery one: **a key is charged at most once.** It is not a promise that a key is dispatched to the provider at most once. Router holds a key against an answer you actually received; the outcomes that charged you nothing release it so the call can be made again. A `5xx`, a `408`/`425`/`429`, and — this is the one that matters here — a call where nothing reached you at all: each of those releases the key, and a retry with it genuinely re-runs and re-dispatches the provider. + +**So a dropped connection is the case idempotency does *not* rescue.** A connection lost mid-call usually means no response was ever committed to you, which is exactly the release path above: retrying with the same key starts a fresh run rather than handing you the result you missed, and if the original generation had already been dispatched the provider may run it a second time. That is the right default — an unbilled call you never received should be re-runnable — but plan for "retry produces a new run", not "retry collects the lost one". + +When Router *does* hold something for the key, the retry is answered rather than re-run: either the original response replayed, or a `409` explaining why it cannot be. A retry sent while the original is still in flight is a `409` carrying `Retry-After`, so wait and re-send the same key. A retry against a call that completed but whose response Router could not keep a faithful copy of is also a `409` — and that is not only the oversized-response case: a response past the replay cap, a handler that failed or panicked after answering, and a write to you that failed or came up short all record the key as consumed-but-not-replayable and return the same `409`. Do not go hunting for a size problem when you see it. The guidance in every one of those cases is the same: use a **new** key. The original completed and was charged, and Router will neither invent its response nor re-run it under the old key. + + +**Not yet in the generated contract.** The `Idempotency-Key` request header, the `409` response and the `Idempotent-Replayed` and `Retry-After` response headers described here are not declared on `POST /v1/models/{provider}/{model}` in the OpenAPI contract the reference is generated from, so they do not appear in the generated API reference and the SDKs do not model them. Send and read them yourself until they do. + + +**Status: not yet.** Durable, resumable execution is expected to arrive with the queued path, which is where a request record has somewhere to live. Idempotent retry is the answer today and is not a stopgap — it is worth wiring in regardless. + +## 呼び出しはサーバーの期限で打ち切られる + +Router の1回の呼び出しは、接続を **10分間** 保持することがあります。これがデフォルト値です。これは固定定数ではなく、サーバー側の設定値であるため、契約に刻まれた保証ではなく、設計上の想定値として扱ってください。これを過ぎると、Router は待機を停止し、プロバイダーへの進行中のリクエストをキャンセルして、`X-Comfy-Error-Type: deadline_exceeded` を伴う `504` を返します。**`deadline_exceeded` の呼び出しは課金されません**: 制限は当社側のものであり、そのコストも当社側が負担します。 + +キャンセルが行わないことが2つあり、どちらも再試行する前に知っておく価値があります。キャンセルは、プロバイダーがすでに受け付けた生成を取り消すことはありません。Router がジョブを送信してポーリングする方式で駆動するパートナーの場合、期限の満了は Router 自身の待機を終了させるだけで、プロバイダーの作業は終了しません。そのため、そのジョブは完了まで実行され、再試行によって **2回目の生成** が発生する可能性があります(その場合も、タイムアウトした呼び出しに対しては課金されません)。また、送信済みの回答を取り消すこともできません。ハンドラーが競争に勝ち、期限が切れるちょうどその瞬間に応答をコミットした場合、`504` ではなくその応答が保持されます。 + +これを、もう一方の `504` と混同しないでください。`provider_timeout` はパートナーが時間内に応答しなかったことを意味し、こちら **は** 課金されます。一方、`deadline_exceeded` は Router 自身の制限が満了したことを意味します。原因が2つ、課金結果も2つあるからこそ、同じステータスコード上に2つのバケットが存在します。`X-Comfy-Error-Type` で分岐し、ステータスコードのみで判断してはいけません。 + +**代わりにすべきこと。** クライアントの読み取りタイムアウトは、期限より *上* に余裕を持って設定してください。期限より下に設定してはいけません。先に諦めるクライアントは、リクエスト識別子を伴う型付きの `504` を不透明なローカル中断に変えてしまい、サポートが追跡できる唯一の証跡を失うことになります。単一の生成が期限内にどうしても完了できない場合、Router は現時点ではその用途に適していません。その場合は、ジョブを送信してポーリングするパートナープロキシルート経由で実行するか、作業を、それぞれが期限内に完了する複数の呼び出しに分割してください。 + +**ステータス: 意図的な設計。** 制限は存在しなければなりません。制限がなければ、スタックしたアップストリームが接続と並行処理スロットを無期限に保持することになります。具体的な数値は調整される可能性がありますが、期限の存在自体がなくなることはありません。 + +## 呼び出し実行中は進捗がありません + +`POST /v1/models/{provider}/{model}` は、最後に一度だけ応答を返します。ストリーミング応答も、サーバー送信イベントも、進捗率も、部分的なフレームやプレビューフレームもありません。これは、自社のAPIが送信とポーリング方式であるパートナーについても当てはまります。Routerはそのポーリングを、あなたの1回の呼び出しの中で内部的に実行し、そこで見られる中間状態はあなたには転送されません。外から見ると、3秒の画像と6分のビデオは同じ形状です。つまり、1つのリクエスト、1つのレスポンス、その間に何もない、ということです。 + +**代わりにできること。** 現在のRouterでは、何もできません。取得できないパーセンテージの代わりに、不確定な進捗状態を表示してください。特定のプロバイダーで進捗が必須要件である場合は、そのプロバイダーのパートナープロキシルートが独自のポーリングやストリーミングを公開しているかどうかを確認し、それらを直接使用してください。実際にいくつかのプロバイダーは対応しており、それらは変更されておらず、完全にサポートされています。 + +**ステータス: 未対応。** これはキュー中パスに結びついています。進捗を報告するための場所が必要ですが、それを提供するのはキュー中の送信であり、単一の同期呼び出しではありません。 + +## 3つの予測バケットは語彙に含まれていない + +Routerの`error_type`語彙は**15個のバケットからなる閉じた集合**です。その15個とは、[APIリファレンス](/ja/api-reference/comfy-router/reference)が列挙し、クイックスタートが指し示すものです。さらに3つが、そのリファレンスの本文で追加が見込まれるものとして名指しされています:`file_download_error`、`cancelled`、`queue_timeout` です。それらは名指しされているだけで、それ以上のものではありません。それらは**現在の集合のメンバーではありません**。Routerのレスポンスがそのいずれかを運ぶことはなく、コントラクトから生成されたクライアントはこれらを認識せず、Routerが内部的にそのいずれかを受け取った場合、レスポンスとして送信する代わりに`internal_error`に置き換えます。したがって、今日これらに対して書く分岐は決して実行されない分岐であり、リファレンスに登場することは、Routerが呼び出しをキャンセルしたりキューに入れたりする証拠ではありません。Routerはどちらも行いません。 + +これらが完全に省かれるのではなく文書で予測されているのは、`error_type`が意図的に単なる文字列であり`enum`ではないためです。認識できないバケットを厳格に拒否するクライアントは、何かがすでにうまくいかなくなったまさにそのときに、最も深刻な失敗をします。追加分を前もって名指しすることで、読者はこの集合が設計上オープンエンドであることを知ることができます。 + +**代わりにすべきこと。** Routerが実際に公開する15個のバケットを処理してください。その完全なリストは[APIリファレンス](/ja/api-reference/comfy-router/reference)にあります。そして、認識できない値を`internal_error`として扱うフォールバック分岐を1つ書いてください。そのフォールバックが仕組み全体です。これにより、この3つと、クライアント作成後に追加される任意のバケットが、あなたを壊さずに届きます。制御フローには大まかなバケットで分岐し、具体的な理由が必要な場合は`422`ボディ内のフィールドごとの`type`を読んでください。 + +**ステータス:未実装。** この3つのそれぞれは、Routerがまだ持っていない動作に対応しており、それぞれがそれを出力し始めるのと同じ変更で語彙に加わります。それより前には決して加わりません。 + +## Router does not cover every partner operation + +Routerはパートナーの*モデル*を実行します。Routerは、パートナーが公開するすべての操作をカバーするわけではありません。ファイルのアップロード、アカウントとアセットの読み取り、プロバイダー固有の管理呼び出し、ストリーミングチャットエンドポイント、一部のパートナーが公開する送信とポーリングのペアなどは対象外です。また、Routerはそれらを再形成することもありません。モデルのネイティブな入力を転送し、ネイティブな出力をそのまま返すため、サポートされていない操作を移植するための統一エンベロープはありません。 + +**代わりにすべきこと。** `/proxy/…` のパートナープロキシルートは、同じホスト上で同じ認証情報を使い、引き続き完全にサポートされています。Routerがカバーしないものには、このルートが答えです。これらは非推奨でも、廃止予定でもありません。同じ統合内でRouterと併用することは、回避策ではなく想定された使い方です。多くのモデルにわたって1つのルート形状と1つの認証情報を使いたい場合はRouterを選んでください。特定のパートナー操作、プロバイダー独自のストリーミング応答、またはRouterが意図的に隠している送信・ポーリング制御が必要な場合は、`/proxy/…` を選んでください。 + +**ステータス: 意図的な設計です。** Routerは対象範囲を意図的に絞っています。1つのルート形状がその機能です。プロキシの対象範囲は現状のままです。 + +## 次のステップ + +- [Comfy Router クイックスタート](/ja/api-reference/comfy-router/quickstart): Python または TypeScript で最初に動作する呼び出しを紹介します。 +- [Comfy Router API リファレンス](/ja/api-reference/comfy-router/reference): Router が送信するすべてのエンドポイント、すべてのパラメータ、すべてのエラーバケットを網羅しています。 diff --git a/ja/api-reference/comfy-router/quickstart.mdx b/ja/api-reference/comfy-router/quickstart.mdx new file mode 100644 index 000000000..aec4614c4 --- /dev/null +++ b/ja/api-reference/comfy-router/quickstart.mdx @@ -0,0 +1,309 @@ +--- +title: "Comfy Router クイックスタート" +description: "Comfy Routerに対して、PythonとTypeScriptで、ゼロから約5分で生成済み画像まで到達する手順を説明します。" +translationSourceHash: 1825ee75 +translationFrom: api-reference/comfy-router/quickstart.mdx +translationBlockHashes: + "_intro": fd4302ce + "Why this page uses `bfl/flux-2-pro`": 04e30793 + "Get a key": 447aa37e + "cURL": bc3e1e4c + "Python": 059e95b8 + "TypeScript": fed849f1 + "Reading the `422`": 6d59c614 + "Where the model's fields come from": 157d882f + "Next": ce5a29fb +--- + +**Comfy Router はまだ一般提供されていません。** 以下のルート +`POST /v1/models/{provider}/{model}` と、そのカタログおよびスキーマの関連ルートは、 +まだリクエストを処理していません。現在、認証付きの呼び出しは `404` を返します。このページは、 +これらのルートが将来提供する契約を文書化したものであり、ロールアウトに先立って公開されているため、 +統合をその契約に合わせて作成する準備ができます。これは、現在実際に試すことができる動作の説明ではありません。 + + +Comfy Router は、パートナーモデルを1つのホスト、1つの資格情報、1つのルート形状の背後で実行します。このページは、生成済み画像への最短の完全なパスです。クライアントをインストールし、キーを設定し、1つのリクエストを送信し、結果を読み取り、そして最初の失敗に遭遇する前に、その失敗がどのようなものかを確認できます。 + +ベース URL: `https://api.comfy.org`。ルートは `POST /v1/models/{provider}/{model}` です。リクエストボディはモデル独自のネイティブ JSON 入力であり、`200` 応答にはモデル独自のネイティブ JSON 出力が含まれます。Router は入力と出力のどちらもラップしないため、パートナーの API に対して既に作成した呼び出しは、ホストを変更するだけで Router の呼び出しになります。 + +## このページで `bfl/flux-2-pro` を使用する理由 + +`bfl/flux-2-pro` は p50 で約 3.1 秒で応答を返します。これは Router 上で測定された中で最速の経路であり、5 分での最初の結果を現実的にするものです。より遅いモデルでは、その予算は読むことではなく待つことに費やされることになるでしょう。 + +これは便宜上のものであり、必須ではありません。Router 上の他のすべてのモデルもまったく同じ方法で呼び出されます。同じルート、同じ認証情報ヘッダー、同じエラーバケット、同じ `X-Comfy-Request-Id` です。変更されるのは、モデル ID、リクエスト本文内のフィールド、そして読み取る結果の形状だけです。たとえば Gemini は、p95 の 72.8 秒を余裕を持ってクリアします。Router は、ポーリング用のジョブハンドルを返すのではなく、生成全体にわたって接続を保持します。長時間の呼び出しを途中で打ち切るエッジ側の上限はありませんが、Router は呼び出し自体に制限を設けています。Router 自身のサーバーデッドライン(デフォルトで 10 分)が接続を保持する最長の時間であり、それを過ぎると `504` / `deadline_exceeded` を返し、課金は行われません。ID を差し替えて、そのモデルのフィールドを(後述の)モデル自身のスキーマから読み取ってください。 + +## APIキーを取得する + +RouterはComfy APIキーで認証します。[platform.comfy.org/profile/api-keys](https://platform.comfy.org/profile/api-keys) で作成し、環境変数に設定してください。以下の2つのサンプルはどちらも `COMFY_API_KEY` を読み取り、キーをリテラルとして受け取らないため、コピー&ペーストしたスニペットが認証情報をコミットに持ち込むことはありません。 + +```bash +export COMFY_API_KEY="comfyui-..." +``` + + +`comfyui-` キーは **`X-API-Key`** ヘッダーで送信してください。`Authorization: Bearer` ではありません。 +2つのヘッダーは異なるバリデータを選択します。`X-API-Key` は `comfyui-` キーを受信時に読み取る唯一のヘッダーであり、`Authorization` 内の値は JWT ブランチにルーティングされます。そこでは、非JWTトークンはターミナルの `401 Invalid token` となり、キーは決して参照されません。(`Authorization: Bearer` は Cloud/Firebase の **JWT** に対して正しい方法です。これは、生成済みの [APIリファレンス](/ja/api-reference/comfy-router/reference) が「bearer token」という言葉で意味しているものです。) + + +キーはワークスペースごとに作成され、そのワークスペースのモデル利用権限とクレジット残高を保持します。有効な認証情報のないリクエストは `401` と `X-Comfy-Error-Type: unauthorized` を返します。ワークスペースがモデルを実行できないリクエストは、`403` / `forbidden` を返します。 + +## cURL + +スクリプト、スモークテスト、ターミナルへのコピー&貼り付けに最適な、最短の呼び出し方法です: + +```bash +curl https://api.comfy.org/v1/models/bfl/flux-2-pro \ + -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"}' +``` + +レスポンスはモデルのネイティブ出力で、以下のサンプルがそのまま読み取る形式です。失敗時は、ボディにエラーが含まれ、`X-Comfy-Error-Type` ヘッダーがエラーの分類を示します。後で問い合わせる必要があるレスポンスからは、`X-Comfy-Request-Id` ヘッダーを保存しておいてください。macOS と Linux にはどちらも `uuidgen` が同梱されています。Windows では、`New-Guid` または任意の UUID ソースを使用して Idempotency-Key を生成してください。 + +## Python + +Requires Python 3.9+ and `httpx`: + +```bash +pip install httpx +``` + +Save as `quickstart.py` and run it with `python quickstart.py`: + +```python +import os +import uuid + +import httpx + +BASE_URL = os.environ.get("COMFY_ROUTER_BASE_URL", "https://api.comfy.org") +MODEL = "bfl/flux-2-pro" + +# Give the client headroom ABOVE Router's own server deadline (10 minutes by +# default) so a call that reaches the server bound comes back as a typed 504 +# with a request id rather than as an opaque client abort. The deadline bounds +# how long Router holds the connection, not whether the call is billed: if the +# provider completed the generation, it is billed either way. +READ_TIMEOUT_SECONDS = 660.0 + + +class RouterError(Exception): + """A Comfy Router failure, typed by its X-Comfy-Error-Type bucket.""" + + def __init__(self, response: httpx.Response) -> None: + self.error_type = response.headers.get("X-Comfy-Error-Type", "internal_error") + self.request_id = response.headers.get("X-Comfy-Request-Id") + self.status_code = response.status_code + # Parse defensively: an error can arrive as an HTML 502 from a load + # balancer, a plain-text 429, an empty body or a truncated JSON one. The + # status, the bucket and the request id above are the parts worth + # keeping, so a body that will not parse must not replace this exception + # with a JSONDecodeError and lose them. + body = None + if response.headers.get("content-type", "").startswith("application/json"): + try: + body = response.json() + except ValueError: + body = None + detail = body.get("detail") if isinstance(body, dict) else None + # A 422 carries a detail[] array - one entry per rejected field, each + # keeping its own `loc`, `msg` and `type`. Every other bucket carries a + # plain `detail` string. + self.errors = detail if isinstance(detail, list) else [] + self.detail = detail if isinstance(detail, str) else f"HTTP {response.status_code}" + super().__init__(self.detail) + + +def run(model: str, arguments: dict, idempotency_key: str) -> dict: + # Idempotency-Key makes a retry safe on a PAID call: Router replays the + # original response for 24h instead of dispatching (and billing) the + # provider a second time. Reuse the SAME key when retrying one logical + # call; generate a new one for a new call. + response = httpx.post( + f"{BASE_URL}/v1/models/{model}", + headers={ + "X-API-Key": os.environ["COMFY_API_KEY"], + "Idempotency-Key": idempotency_key, + }, + json=arguments, + timeout=httpx.Timeout(READ_TIMEOUT_SECONDS, connect=10.0), + ) + if response.is_error: + raise RouterError(response) + return response.json() + + +result = run( + MODEL, + {"prompt": "a red teapot on a windowsill, morning light"}, + idempotency_key=str(uuid.uuid4()), +) +# Router forwards each provider's native output unchanged, so this path is +# BFL's, not a Router envelope. Reading a different model means reading its own +# output shape. +print("image:", result["result"]["sample"]) + +# The first failure most callers hit: a field the model's input schema requires +# is missing, so Router rejects the request BEFORE any provider call - which is +# why a 422 is never billed. +try: + run(MODEL, {"width": 1024}, idempotency_key=str(uuid.uuid4())) +except RouterError as exc: + print(f"{exc.error_type} (HTTP {exc.status_code}), request id {exc.request_id}") + for entry in exc.errors: + print(" ", ".".join(str(p) for p in entry["loc"]), "->", entry["msg"]) +``` + +```text +image: https://.../out.jpeg +invalid_input (HTTP 422), request id 6f1c... + body.prompt -> Field required +``` + +## TypeScript + +Requires Node 18+ (for built-in `fetch`, `AbortSignal.timeout` and `crypto.randomUUID`) and `tsx` to run TypeScript directly: + +```bash +npm install --save-dev tsx +``` + +Save as `quickstart.mts` — the `.mts` extension is load-bearing, because the file uses top-level `await` and that needs an ES module — and run it with `npx tsx quickstart.mts`: + +```typescript +const BASE_URL = process.env.COMFY_ROUTER_BASE_URL ?? "https://api.comfy.org"; +const MODEL = "bfl/flux-2-pro"; + +// Headroom ABOVE Router's own server deadline (10 minutes by default), so a +// call that reaches the server bound returns a typed 504 with a request id +// rather than aborting locally at the same moment. The deadline bounds how +// long Router holds the connection, not whether the call is billed: if the +// provider completed the generation, it is billed either way. +const CLIENT_TIMEOUT_MS = 660_000; + +interface ValidationEntry { + loc: (string | number)[]; + msg: string; + type: string; +} + +/** A Comfy Router failure, typed by its `X-Comfy-Error-Type` bucket. */ +class RouterError extends Error { + readonly errorType: string; + readonly requestId: string | null; + readonly status: number; + /** A 422 carries a `detail[]` array — one entry per rejected field, each + * keeping its own `loc`, `msg` and `type`. Every other bucket carries a + * plain `detail` string. */ + readonly errors: ValidationEntry[]; + + constructor(response: Response, body: unknown) { + const detail = + typeof body === "object" && body !== null + ? (body as { detail?: unknown }).detail + : undefined; + super(typeof detail === "string" ? detail : `HTTP ${String(response.status)}`); + this.name = "RouterError"; + this.errorType = response.headers.get("X-Comfy-Error-Type") ?? "internal_error"; + this.requestId = response.headers.get("X-Comfy-Request-Id"); + this.status = response.status; + this.errors = Array.isArray(detail) ? (detail as ValidationEntry[]) : []; + } +} + +/** Read a body without letting a non-JSON error page mask the real failure. */ +async function parseBody(response: Response): Promise { + const text = await response.text(); + try { + return JSON.parse(text) as unknown; + } catch { + return undefined; + } +} + +async function run( + model: string, + args: Record, + idempotencyKey: string, +): Promise { + // Idempotency-Key makes a retry safe on a PAID call: Router replays the + // original response for 24h instead of dispatching (and billing) the provider + // a second time. Reuse the SAME key when retrying one logical call. + const response = await fetch(`${BASE_URL}/v1/models/${model}`, { + method: "POST", + headers: { + "X-API-Key": process.env.COMFY_API_KEY ?? "", + "Idempotency-Key": idempotencyKey, + "Content-Type": "application/json", + }, + body: JSON.stringify(args), + signal: AbortSignal.timeout(CLIENT_TIMEOUT_MS), + }); + // Branch on `ok` FIRST: an HTML 502, a plain-text 429 or an empty body must + // still surface the status, the bucket and the request id. + const body = await parseBody(response); + if (!response.ok) throw new RouterError(response, body); + return body as T; +} + +const result = await run<{ result: { sample: string } }>( + MODEL, + { prompt: "a red teapot on a windowsill, morning light" }, + crypto.randomUUID(), +); +// Router forwards each provider's native output unchanged, so this path is +// BFL's, not a Router envelope. Reading a different model means reading its own +// output shape. +console.log("image:", result.result.sample); + +// The first failure most callers hit: a field the model's input schema requires +// is missing, so Router rejects the request BEFORE any provider call - which is +// why a 422 is never billed. +try { + await run(MODEL, { width: 1024 }, crypto.randomUUID()); +} catch (exc) { + if (!(exc instanceof RouterError)) throw exc; + console.log(`${exc.errorType} (HTTP ${String(exc.status)}), request id ${String(exc.requestId)}`); + for (const entry of exc.errors) console.log(" ", entry.loc.join("."), "->", entry.msg); +} +``` + +```text +image: https://.../out.jpeg +invalid_input (HTTP 422), request id 6f1c... + body.prompt -> Field required +``` + +## `422` の読み方 + +`422` は、最初の実呼び出しの前に理解しておく価値がある唯一のエラーです。なぜなら、それは自分自身が引き起こすエラーだからです。これは、Router がボディをモデル自身の入力スキーマに対して検証し、拒否したことを意味します。つまり、必須フィールドが不足している、値が範囲外、画像が小さすぎる、といったケースです。このチェックはプロバイダー呼び出しの前に実行されるため、`422` はコストがかかりません。パートナーの支出もなく、後で請求に関する質問に答える必要もありません。これは `400` とは異なります。`400` はリクエストレベルの失敗(不正なカーソル、読み取れないエンベロープ)であり、フィールド単位の失敗ではありません。 + +そのボディは fal/FastAPI の `detail[]` 形状です。問題のあるフィールドごとに1つのエントリを持つ配列で、各エントリは独自の `loc`(フィールドへのパス)、`msg`、`type`(プロバイダーレベルの具体的な理由: `missing`、`value_error`、`image_too_small`)、および理由に境界が含まれる場合は `ctx` を保持します。このフィールド単位の粒度こそが、上記のサンプルが配列を例外メッセージにフラット化せずにデータとして保持する理由です。 + + +入力スキーマがまだ作成されていないモデルは、任意の JSON オブジェクトを受け入れる文書化された +寛容なフォールバックとして解決されるため、`422` を返す代わりにボディを転送します。 +上記のサンプルは、スキーマが存在する場合に処理する形状を示しています。`422` ブロックは、 +その特定のボディに対する保証された応答ではなく、エラーパスとして扱ってください。 + + +このボディには独自の `error_type` フィールドがないため、`422` では `X-Comfy-Error-Type` ヘッダーが*唯一の*機械可読なバケットになります。両方のサンプルはまさにその理由から、ヘッダーからバケットを最初に読み取ります。これにより、Router が返すすべての失敗を1つのエラークラスでカバーできます。 + +`X-Comfy-Request-Id` は、成功、`4xx`、`5xx` を問わずすべてのレスポンスに含まれており、サポートリクエストで引用する ID です。両方のサンプルは、ヘッダーロギングを有効にして再実行する代わりに、例外に ID を添付します。 + +## モデルのフィールドの由来 + +`prompt` は `bfl/flux-2-pro` が必須とする唯一のフィールドです。次に必要になるのは `width`、`height`、`seed`、`output_format` です。時間とともにずれる可能性のあるフィールド一覧を再掲する代わりに、モデルのスキーマをライブで確認してください: + +```bash +curl -H "X-API-Key: $COMFY_API_KEY" \ + https://api.comfy.org/v1/models/bfl/flux-2-pro/openapi.json +``` + +これは、サーバーがあなたの呼び出しの検証に使用するものと同じドキュメントで、スタンドアロンのOpenAPIドキュメントとして提供されます。そのため、公開されている仕様と実際に強制される仕様が食い違うことはありません。任意のモデルIDを選び、その呼び出しパスに `/openapi.json` を追加すれば、返ってきた内容に基づいて生成できます。 + +## 次のステップ + +- [Comfy Router API リファレンス](/ja/api-reference/comfy-router/reference): すべてのエンドポイント、すべてのパラメータ、そして 15 種類すべてのエラー分類を網羅しています。 +- [Comfy Router の制限事項](/ja/api-reference/comfy-router/limitations): 現在 Router が対応していない機能と、その代わりに使用すべきものを説明しています。 diff --git a/ja/api-reference/comfy-router/reference.mdx b/ja/api-reference/comfy-router/reference.mdx new file mode 100644 index 000000000..218e2c6f8 --- /dev/null +++ b/ja/api-reference/comfy-router/reference.mdx @@ -0,0 +1,267 @@ +--- +title: "Comfy Router API リファレンス" +description: "Comfy API 契約から生成済みの、Comfy Router のすべてのエンドポイント、パラメータ、レスポンスボディ、エラーバケット。" +translationSourceHash: 1e8777df +translationFrom: api-reference/comfy-router/reference.mdx +translationBlockHashes: + "_intro": 0114a881 + "Endpoints": 6bc355f0 + "Error buckets": 04a58305 + "Response headers": 2a099bc2 + "Per-model input schemas": ae73e63b + "Schemas": a14d076e +--- + +{/* + 生成済みファイル: 手動で編集しないでください。 + + gen_router_reference.py によって Comfy API コントラクトから生成されています。 + コントラクトを編集して再生成してください。ここでの編集は次回の実行で上書きされ、 + それまでの間はドリフトゲートによって拒否されます。 +*/} + +モデル ID でアドレス指定される Comfy Router の正規ルート。 + +ベース URL: `https://api.comfy.org` + +以下のすべてのエンドポイントは認証が必要です。`Authorization: Bearer ` を送信してください。 + +## エンドポイント + +### `GET /v1/models` + +**Comfy Router が実行できるモデルを一覧表示します。** + +Comfy Router のモデルカタログ。`POST /v1/models/{provider}/{model}` が受け付ける正規モデル ID の 1 ページ分です。SDK はコールドスタート時にこの API を呼び出して実行可能なモデルを検出し、`model_not_found` の提案も同じカタログから取得されます。したがって、ここに掲載されている ID が呼び出し時に 404 になる場合は、どちらか一方の失敗だけよりも悪い結果になります。この一致は約束ではなく構造上のものです。エントリの `provider` と `model` は、呼び出しルートの2つのパスセグメントであり、そのルートのパスパラメータと同じスキーマコンポーネントを参照します。また、`id` はそれらの2つのセグメントを `/` で連結したものです。 + +**パラメータ** + +| 名前 | 場所 | 必須 | 型 | 制約 | 説明 | +| --- | --- | --- | --- | --- | --- | +| `cursor` | クエリ | いいえ | [`RouterPageCursor`](#routerpagecursor) | `pattern: ^[A-Za-z0-9._~+/=-]+$`, `minLength: 1`, `maxLength: 512` | 不透明なページネーションカーソル。前のページの `next_cursor` を渡すと次のページを取得できます。最初のページでは省略します。値が不透明である理由と、このルートがオフセットではなくカーソルでページネーションする理由については、`RouterPageCursor` を参照してください。 | +| `limit` | クエリ | いいえ | integer | `maximum: 100`, `default: 20` | 1 ページで返すモデル数。宣言された最大値を超える値は契約外ですが、このルートはそれらを拒否しません。代わりに最大値を返し、実際に返されるページサイズはレスポンスの `limit` としてエコーバックされるため、クランプは常に呼び出し側が検出できます。最大値を実際のページストライドとして扱ってください。より多くを要求し、より多くを受け取ったと想定するクライアントは行を失います。0 と負の値も受け入れられ、デフォルトを選択します。そのため `minimum` は宣言されていません。1 未満はここでは意味があり、無効ではありません。 | + +**レスポンス** + +| ステータス | ボディ | ヘッダー | 説明 | +| --- | --- | --- | --- | +| `200` | [`RouterModelListResponse`](#routermodellistresponse) | `X-Comfy-Request-Id` | OK: モデルカタログの 1 ページ分。 | +| `400` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | Router のリクエストレベルの失敗。リクエストがモデルに到達しなかったか、モデル自身が報告しない理由で失敗しました。ボディは `RouterErrorResponse` で、バケットは `X-Comfy-Error-Type` に繰り返されます。 | +| `401` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | Router のリクエストレベルの失敗。リクエストがモデルに到達しなかったか、モデル自身が報告しない理由で失敗しました。ボディは `RouterErrorResponse` で、バケットは `X-Comfy-Error-Type` に繰り返されます。 |### `GET /v1/models/{provider}/{model}` + +**正規のモデル ID で、パートナーモデルのカタログエントリを 1 件読み取ります。** + +単一の Comfy Router モデルに対するモデル単位の詳細です。呼び出し元は、ページ分割されたカタログ全体を走査しなくても、1 つのモデルを確認できます。SDK はモデルを呼び出す直前に、このエンドポイントを使用してモデルを検索します。 + +**パラメータ** + +| 名前 | 場所 | 必須 | 型 | 制約 | 説明 | +| --- | --- | --- | --- | --- | --- | +| `provider` | パス | はい | [`RouterProviderSegment`](#routerprovidersegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 64` | 正規の `{provider}/{model}[/{variant}]` モデル ID における小文字のプロバイダーセグメントです。実行されるモデルのパートナーを示します。 | +| `model` | パス | はい | [`RouterModelSegment`](#routermodelsegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 128` | 正規の `{provider}/{model}[/{variant}]` モデル ID における小文字のモデルセグメントです。そのプロバイダー内で実行するモデルを示します。 | + +**レスポンス** + +| ステータス | ボディ | ヘッダー | 説明 | +| --- | --- | --- | --- | +| `200` | [`RouterModelDetail`](#routermodeldetail) | `X-Comfy-Request-Id` | OK: モデルのカタログエントリです。 | +| `404` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | Router のリクエストレベルでの失敗です。リクエストがモデルに到達しなかったか、モデル自身が報告しなかった理由で失敗したことを示します。ボディは `RouterErrorResponse` で、バケットは `X-Comfy-Error-Type` ヘッダーにも繰り返し含まれます。 |### `POST /v1/models/{provider}/{model}` + +**正規モデルIDでパートナーモデルを同期的に実行します。** + +Comfy Routerの正規のエントリポイントであり、モデルIDでアドレス指定されます。リクエストボディはパートナーモデル自身のネイティブなJSON入力であり、成功レスポンスはそのモデル自身のネイティブなJSON出力です。RouterはComfy形式のエンベロープを押し付けるのではなく、両方をそのまま転送するため、呼び出し元はホストを変更するだけでパートナーのAPIとRouterを切り替えることができます。これは同期パスであり、`POST https://fal.run/{id}` をミラーリングします。レスポンスには完了した結果が含まれます。キュー処理用の対応エンドポイントである `/v1/queue/models/{provider}/{model}` が計画されており、falの `fal.run` と `queue.fal.run` の分割を単一のホストにまとめることになります。ただし、これはまだこの契約の一部ではありません。 + +**パラメータ** + +| 名前 | 場所 | 必須 | 型 | 制約 | 説明 | +| --- | --- | --- | --- | --- | --- | +| `provider` | path | yes | [`RouterProviderSegment`](#routerprovidersegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 64` | 正規の `{provider}/{model}[/{variant}]` モデルIDの小文字のプロバイダーセグメント。モデルが実行されるパートナーを示します。 | +| `model` | path | yes | [`RouterModelSegment`](#routermodelsegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 128` | 正規の `{provider}/{model}[/{variant}]` モデルIDの小文字のモデルセグメント。そのプロバイダー内で実行するモデルを示します。 | + +**リクエストボディ** + +`application/json` - [`RouterModelInput`](#routermodelinput)(必須) + +パートナーモデルのネイティブなJSON入力で、プロバイダーにそのまま転送されます。 + +**レスポンス** + +| ステータス | ボディ | ヘッダー | 説明 | +| --- | --- | --- | --- | +| `200` | [`RouterModelOutput`](#routermodeloutput) | `X-Comfy-Request-Id` | OK。パートナーモデルのネイティブなJSON出力がそのまま返されます。 | +| `403` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | Routerのリクエストレベルの失敗です。リクエストがモデルに到達しなかったか、モデル自身が報告しない理由で失敗しました。ボディは `RouterErrorResponse` であり、そのバケットは `X-Comfy-Error-Type` にも繰り返し記載されます。 | +| `404` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | Routerのリクエストレベルの失敗です。リクエストがモデルに到達しなかったか、モデル自身が報告しない理由で失敗しました。ボディは `RouterErrorResponse` であり、そのバケットは `X-Comfy-Error-Type` にも繰り返し記載されます。 | +| `422` | [`RouterValidationErrorResponse`](#routervalidationerrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | リクエストはモデルに到達しましたが、モデルはその内容を拒否しました。ボディは `RouterValidationErrorResponse` であり、fal/FastAPIの `detail[]` 形状です。そのため、各問題のあるフィールドは独自の `type` と `ctx` を保持します。`X-Comfy-Error-Type` はレスポンス全体の大まかなバケットを伝えます。 | +| `503` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | Routerのリクエストレベルの失敗です。リクエストがモデルに到達しなかったか、モデル自身が報告しない理由で失敗しました。ボディは `RouterErrorResponse` であり、そのバケットは `X-Comfy-Error-Type` にも繰り返し記載されます。 | +| `504` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | Routerのリクエストレベルの失敗です。リクエストがモデルに到達しなかったか、モデル自身が報告しない理由で失敗しました。ボディは `RouterErrorResponse` であり、そのバケットは `X-Comfy-Error-Type` にも繰り返し記載されます。 |### `GET /v1/models/{provider}/{model}/openapi.json` + +**1つのパートナーモデルの入力スキーマをOpenAPIドキュメントとして読み取ります。** + +単一のComfy Routerモデルのモデルごとの入力スキーマは、スタンドアロンのOpenAPIドキュメントとして提供されます。これにより、呼び出し側(SDK、コード生成ツール、またはエージェント)は、Comfyの解説ドキュメントを読まなくてもモデルの引数を発見できます。これはfalのモデルごとのスキーマエンドポイントを反映したものであり、SDKクイックスタートが依存する発見メカニズムです。 + +**パラメータ** + +| 名前 | 場所 | 必須 | 型 | 制約 | 説明 | +| --- | --- | --- | --- | --- | --- | +| `provider` | path | yes | [`RouterProviderSegment`](#routerprovidersegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 64` | 正規の `{provider}/{model}[/{variant}]` モデルIDの小文字のプロバイダーセグメント。実行されるモデルのパートナーです。 | +| `model` | path | yes | [`RouterModelSegment`](#routermodelsegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 128` | 正規の `{provider}/{model}[/{variant}]` モデルIDの小文字のモデルセグメント。そのプロバイダー内で実行するモデルです。 | + +**レスポンス** + +| ステータス | ボディ | ヘッダー | 説明 | +| --- | --- | --- | --- | +| `200` | [`RouterModelInputSchemaDocument`](#routermodelinputschemadocument) | `X-Comfy-Request-Id`, `ETag`, `Cache-Control` | OK: モデルの入力スキーマをスタンドアロンのOpenAPIドキュメントとして返します。 | +| `304` | - | `X-Comfy-Request-Id`, `ETag`, `Cache-Control` | Not Modified: 呼び出し側が `If-None-Match` で送信した `ETag` 以降、ドキュメントは変更されていません。ボディは返されません。 | +| `404` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | Routerのリクエストレベルの失敗: リクエストがモデルに到達しなかったか、モデル自身が報告しなかった理由で失敗しました。ボディは `RouterErrorResponse` で、バケットは `X-Comfy-Error-Type` に繰り返されます。 | +| `500` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | Routerのリクエストレベルの失敗: リクエストがモデルに到達しなかったか、モデル自身が報告しなかった理由で失敗しました。ボディは `RouterErrorResponse` で、バケットは `X-Comfy-Error-Type` に繰り返されます。 | + +## エラーバケット + +Router の失敗を示す、粗い粒度の機械可読バケットです。`X-Comfy-Error-Type` レスポンスヘッダーにもミラーリングされるため、呼び出し側はボディを解析せずに分岐できます。セットは15個の値で固定されています。リクエストレベルの6つのバケット `invalid_input`、`content_policy_violation`、`provider_error`、`provider_timeout`、`insufficient_credits`、`model_not_found` に加え、トランスポートレベルの `unauthorized`、`forbidden`、`concurrency_limit_exceeded`、`client_disconnected`、`internal_error`、`deadline_exceeded`、`not_enabled`、`service_unavailable`、`rate_limited` です。 + +### リクエストレベルバケット + +Router が受け付けたものの完了できなかったリクエストに対して発生します。 + +| `error_type` | 意味 | +| --- | --- | +| `invalid_input` | リクエストがモデルに到達する前に拒否されました。不正な形式のボディ、不正または期限切れのページネーションカーソル、またはモデル自身のスキーマが受け付けない入力が原因です。 | +| `content_policy_violation` | プロバイダーがコンテンツポリシーを理由にリクエストを拒否しました。この拒否は決定的です。同じ入力を再送信しても再び拒否されます。 | +| `provider_error` | パートナープロバイダーが自身の障害を報告したか、Router が結果として解釈できないレスポンスを返しました。 | +| `provider_timeout` | パートナープロバイダーが期限までに応答しませんでした。このバケットはプロバイダーのタイムアウトであり、Router 自身のサーバー側の期限ではありません。サーバー側の期限は `deadline_exceeded` として報告されます。両者は `504` を共有しますが、原因が異なるため区別されます。こちらはパートナーが失敗したことを示し、あちらは Comfy が接続の維持を停止したことを示します。 | +| `insufficient_credits` | 呼び出し元のワークスペースに、モデルを実行するための十分なクレジットがありません。 | +| `model_not_found` | `{provider}/{model}` という ID が、Router で実行できるモデルを指していません。不明なプロバイダーもここに分類されます。`detail` には、呼び出し側が閲覧を許可されているモデルから抽出された最大3件の提案が含まれます。 | + +### トランスポートレベルバケット + +モデルへの呼び出しの前またはその周辺で、Router 自身によって発生します。 + +| `error_type` | 意味 | +| --- | --- | +| `unauthorized` | リクエストに利用可能な認証情報が含まれていませんでした。 | +| `forbidden` | 認証情報は有効ですが、このモデルまたはこの操作に対する権限がありません。 | +| `concurrency_limit_exceeded` | ワークスペースはすでに許可された数の呼び出しを実行中です。いずれかが完了したら再試行してください。 | +| `client_disconnected` | 呼び出し側が、Router が結果を返す前に接続を閉じました。これは配信ではなくログに記録されます。書き込むソケットが残っていないためです。また、これは課金結果ではなく原因の帰属を示すものです。完了したプロバイダーによる生成は、呼び出し側がレスポンスを受信したかどうかに関係なく請求されます。 | +| `internal_error` | Router 自体が失敗しました。これは、クライアントが認識できないバケットを扱う際の値でもあります。これにより、後でセットに追加が行われても、それ以前に生成されたクライアントが壊れることはありません。 | +| `deadline_exceeded` | 回答が到着する前に、Comfy が自身の設定済みの上限で接続の維持を停止しました。`provider_timeout` と `504` を共有し、このペアはどちらの側が時間切れになったかを示します。こちらは Comfy 自身の上限であるため、リクエストのいかなる部分も拒否されておらず、同じリクエストを再試行できます。課金については何も示しません。完了したプロバイダーによる生成は、呼び出し側がレスポンスを受信したかどうかに関係なく請求されます。 | +| `not_enabled` | この呼び出し側に対して Comfy Router がまだ有効になっていません。リクエストに問題はなく、モデルも存在するため、`model_not_found` ではありません。`forbidden` と `403` を共有しますが、同じものではありません。`forbidden` は呼び出し側に関する権限の判断であるのに対し、こちらはロールアウトの状態だからです。これは終端状態です。再試行せず、また障害として扱わないでください。 | +| `service_unavailable` | Comfy Router が依存するサービスが一時的に利用できず、呼び出し側に問題はありません。バックオフを伴って再試行してください。これは、呼び出し側がリクエストを変更することなく、また並行処理スロットが解放されることもなく、条件が自動的に解消される唯一のバケットです。これが他の再試行可能な応答 (`concurrency_limit_exceeded`、`deadline_exceeded`) との違いです。また、`internal_error` とは区別されます。`internal_error` は `500` で、Router 自体が失敗したことを意味します。これにより、クライアントは「すぐに再試行してください」と「この呼び出しは成功しない」を区別できます。 | +| `rate_limited` | 呼び出し側がウィンドウ単位で測定される割り当てを消費し、そのウィンドウが経過するのを待つ必要があります。`concurrency_limit_exceeded` と `429` を共有しますが、同じものではありません。`concurrency_limit_exceeded` は、呼び出し側自身の実行中の呼び出しが1つ完了した時点で解消されるため、数秒以内の再試行が適切です。一方、こちらは呼び出し側が何をしても早く解消されません。`detail` はウィンドウを示します。 | + +## レスポンスヘッダー + +| ヘッダー | 型 | 説明 | +| --- | --- | --- | +| `Cache-Control` | `string` | 提供されるスキーマドキュメントの鮮度ディレクティブ。ルートが認証済みのため `private` です。ドキュメント自体は呼び出し元固有ではありませんが、共有キャッシュは認証済みリクエストへのレスポンスを保持してはなりません。また、`must-revalidate` により、古いコピーはそのまま提供されるのではなく `ETag` に対して再検証されます。 | +| `ETag` | `string` | `GET /v1/models/{provider}/{model}/openapi.json` で提供されるドキュメントのバイト列に対する強力なエンティティタグ。モデルごとのスキーマはほとんど変更されず、SDK が頻繁に再取得するため、呼び出し元はこの値を保存し、`If-None-Match` として送り返すことで、ドキュメントの代わりに `304` を受け取るべきです。 | +| `X-Comfy-Error-Type` | [`RouterErrorType`](#routererrortype) | 障害の大まかな機械可読バケットで、Router がすべてのエラーレスポンスに設定します。`RouterErrorResponse.error_type` と同じ値を保持し、`422` では唯一の機械可読バケットです。これは、そのボディが fal/FastAPI の `detail[]` 形状であり、独自の `error_type` フィールドを持たないためです。したがって、クライアントは、受信した 2 つの Router エラーボディのどちらであるかを判断する前に、このヘッダーだけで分岐できます。 | +| `X-Comfy-Request-Id` | `string` | この呼び出しのサーバー生成識別子で、成功、4xx、5xx を問わず、すべての Router レスポンスに存在します。エラーレスポンスこそ、ユーザーがサポートリクエストで引用する ID を必要とするタイミングだからです。同じ値が呼び出しの使用状況/監査イベントにも書き込まれるため、課金に関する苦情をタイムスタンプで検索する代わりに、課金自体に結び付けることができます。 | + +## モデルごとの入力スキーマ + +モデル独自の入力フィールドはここでは再掲しません。それらは `GET /v1/models/{provider}/{model}/openapi.json` から直接取得できます。このエンドポイントは、サーバーが呼び出しの検証に使用するのと同じドキュメントを提供するため、公開されている内容と実際に適用される内容が乖離することはありません。`GET /v1/models` からモデルIDを取得し、その呼び出しパスに `/openapi.json` を追加して、返されたドキュメントに基づいて生成します。 + +## スキーマ + +### RouterChargesOnPolicyRejection + +このモデルがコンテンツポリシー上の理由で拒否する呼び出しが、それでも呼び出し元に課金されるかどうか。プロバイダーによって異なり、その違いは呼び出し時に判別できません。同じ呼び出しでエラーと課金の両方を目にしたユーザーには、事前にそれを知る手段がありません。そのため、プロバイダーごとの暗黙の了解に委ねるのではなく、呼び出しの前にモデルごとに明記されています。 + +型: `string`### RouterErrorResponse + +Routerのリクエストレベルのエラーボディ: リクエストがモデルに到達しなかった場合、またはモデル自身が報告しなかった理由(認証、クォータ、不明なモデルID、プロバイダーのトランスポート)で失敗した場合に返されるものです。モデルレベルの検証失敗には独自の形状`RouterValidationErrorResponse`があります。FastAPIの`detail[]`配列をこの`detail`文字列に平坦化すると、SDKが分岐の判断に使用するフィールド単位の粒度が失われるためです。 + +| フィールド | 型 | 必須 | 制約 | 説明 | +| --- | --- | --- | --- | --- | +| `detail` | string | はい | - | 失敗の人間が読める説明で、エンドユーザーに表示しても安全です。機械解析されません。代わりに`error_type`で分岐してください。 | +| `error_type` | [`RouterErrorType`](#routererrortype) | はい | - | Routerの失敗を示す大まかな機械可読バケットで、`X-Comfy-Error-Type`レスポンスヘッダーにも反映されるため、呼び出し元はボディを解析せずに分岐できます。このセットは15個の値に固定されています。リクエストレベルの6つのバケット(`invalid_input`、`content_policy_violation`、`provider_error`、`provider_timeout`、`insufficient_credits`、`model_not_found`)に加え、トランスポートレベルの9つのバケット(`unauthorized`、`forbidden`、`concurrency_limit_exceeded`、`client_disconnected`、`internal_error`、`deadline_exceeded`、`not_enabled`、`service_unavailable`、`rate_limited`)があります。 |### RouterErrorType + +Router障害の大まかで機械可読なバケットであり、`X-Comfy-Error-Type` レスポンスヘッダーにもミラーリングされるため、呼び出し元はボディを解析せずに分岐できます。このセットは15個の値に限定されており、リクエストレベルの6つのバケット(`invalid_input`、`content_policy_violation`、`provider_error`、`provider_timeout`、`insufficient_credits`、`model_not_found`)と、トランスポートレベルの`unauthorized`、`forbidden`、`concurrency_limit_exceeded`、`client_disconnected`、`internal_error`、`deadline_exceeded`、`not_enabled`、`service_unavailable`、`rate_limited`です。 + +型: `string`### RouterModelBilling + +呼び出し前に呼び出し元が把握しておくべき、価格ではなくモデル単位の課金に関する事実です。利用量やコストの数値がここに記載されることはありません。 + +| フィールド | 型 | 必須 | 制約 | 説明 | +| --- | --- | --- | --- | --- | +| `charges_on_policy_rejection` | [`RouterChargesOnPolicyRejection`](#routerchargesonpolicyrejection) | yes | - | このモデルがコンテンツポリシーに基づいて拒否した呼び出しが、それでも呼び出し元に課金されるかどうか。プロバイダーによって対応は異なり、その違いは呼び出し時には見えません。同じ呼び出しに対してエラーと課金の両方を確認したユーザーには、事前にそれを知る手段はありません。そのため、この情報はプロバイダーごとの慣習に委ねられるのではなく、呼び出し前にモデル単位で明記されます。 |### RouterModelDetail + +1つのComfy Routerモデルに関するモデルごとの詳細。カタログ一覧で示されるすべての情報に加え、単一モデルルートのみが保持するモデルごとのフィールドを含みます。 + +[`RouterModelListEntry`](#routermodellistentry) と [`RouterModelDetailFields`](#routermodeldetailfields) で構成されます。 + +型:`object`### RouterModelDetailFields + +カタログ一覧が保持しない `RouterModelDetail` の半分: モデルごとのフィールドで、1回の参照には値するものの、ページ分割されたカタログページのすべてのエントリで繰り返すほどではないものです。 + +| フィールド | 型 | 必須 | 制約 | 説明 | +| --- | --- | --- | --- | --- | +| `input_schema_url` | 文字列 | いいえ | `format: uri`, `pattern: ^https://`, `maxLength: 2048` | このモデルの入力スキーマドキュメントへのポインタ: このモデルに対して `POST /v1/models/{provider}/{model}` が受け付けるボディの説明です。この契約の一部となるのはポインタのみです。ポインタが指すドキュメントは別途作成されます。モデル用のスキーマが作成されていない場合は存在しません。 |### RouterModelId + +正規の Comfy Router モデル ID です。`{provider}/{model}` は、`POST /v1/models/{provider}/{model}` でモデルを指定する際に使用する正確な値です。そのため、呼び出し元はこの値をそのままパスに埋め込むことができ、他の情報から再導出する必要はありません。`pattern` は `RouterProviderSegment` と `RouterModelSegment` を単一の `/` で連結したもので、`maxLength` はそれらの合計にそのセパレータを加えた長さです。 + +型: `string`、`pattern: ^[a-z0-9]+([._-][a-z0-9]+)*/[a-z0-9]+([._-][a-z0-9]+)*$`、`maxLength: 193`### RouterModelInput + +パートナーモデルのネイティブな JSON 入力ドキュメントで、プロバイダーにそのまま転送されます。具体的な形状は Comfy ではなくパートナーが所有するため、これはオープンオブジェクトです。Router はフィールドを絞り込んだり、名前を変更したり、再ラップしたりしません。これは名前付きコンポーネントです(インラインの無名オブジェクトにはなりません)。ComfyUI の仕様駆動のコード生成では、生成対象のクラスが必要だからです。 + +型: `object`### RouterModelInputSchemaDocument + +単一の Comfy Router モデルの入力を説明するスタンドアロンの OpenAPI ドキュメントです。これは、そのモデルに対して `POST /v1/models/{provider}/{model}` が受け付けるボディです。`GET /v1/models/{provider}/{model}/openapi.json` が返すのはこのドキュメントです。 + +型: `object`### RouterModelListEntry + +Routerモデルカタログの1エントリです。実行可能なモデルの識別情報であり、それ以外の何ものでもありません。モデルごとの詳細ルートは、この同じエントリを再掲するのではなく合成するため、名前は`...Summary`ではなく`...ListEntry`となっています。カタログエントリの定義は正確に1つだけ存在する必要があります。モデルごとの詳細と、モデルごとの入出力スキーマはそれぞれ独自のルートであるため、この形状は、呼び出し元がモデルを呼び出すために必要な最小限のものに留まっています。これは意図的なものであり、SDKがコールドスタート時に取得するペイロードだからです。`id`は`provider`と`model`を`/`で連結したものです。この2つのフィールドは個別にも保持されるため、呼び出し元は文字列を分割することなく呼び出しパスを構成できます。 + +| フィールド | 型 | 必須 | 制約 | 説明 | +| --- | --- | --- | --- | --- | +| `id` | [`RouterModelId`](#routermodelid) | はい | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*/[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 193` | 正規のComfy RouterモデルID、`{provider}/{model}`です。`POST /v1/models/{provider}/{model}`でモデルを指定する正確な値であり、呼び出し元は何かから再導出することなく、この値をそのパスに挿入できます。その`pattern`は`RouterProviderSegment`と`RouterModelSegment`を単一の`/`で連結したものであり、`maxLength`はそれらの合計にその区切り文字を加えたものです。 | +| `provider` | [`RouterProviderSegment`](#routerprovidersegment) | はい | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 64` | 正規の`{provider}/{model}[/{variant}]`モデルIDの小文字の`provider`セグメントです。つまり、モデルが指定されているパートナーです。呼び出しルートの`provider`パスパラメータとカタログエントリの`provider`フィールドは、どちらもこの1つのスキーマを参照しており、これにより、リストされたIDと受け入れられるIDが乖離しないようになっています。 | +| `model` | [`RouterModelSegment`](#routermodelsegment) | はい | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 128` | 正規の`{provider}/{model}[/{variant}]`モデルIDの小文字の`model`セグメントです。つまり、そのプロバイダー内で実行するモデルです。`RouterProviderSegment`と同じ乖離防止の理由により、呼び出しルートの`model`パスパラメータとカタログエントリの`model`フィールドで共有されています。 | +| `billing` | [`RouterModelBilling`](#routermodelbilling) | はい | - | 呼び出し元が呼び出し前に必要とするモデルごとの請求の事実です。価格ではありません。使用量やコストの数値がここに現れることはありません。 |### RouterModelListResponse + +Routerモデルカタログの1ページ。 + +| フィールド | 型 | 必須 | 制約 | 説明 | +| --- | --- | --- | --- | --- | +| `data` | [`RouterModelListEntry`](#routermodellistentry) の配列 | 必須 | - | このページに含まれるモデル。最大で `limit` 個。 | +| `has_more` | ブール | 必須 | - | このページの先に別のページが存在するかどうか。これが true の間はウォークを続けてください。`data` が短い、または空だからといって、カタログの終端を推測しないでください。 | +| `next_cursor` | [`RouterPageCursor`](#routerpagecursor) | 任意 | `pattern: ^[A-Za-z0-9._~+/=-]+$`, `minLength: 1`, `maxLength: 512` | Routerリストへの不透明カーソル。サーバーによって生成され、ラウンドトリップされるだけです。オフセットでもモデルIDでもなく、順序付けもされておらず、カタログの再構築をまたいで安定することもありません。そのため、カーソルを解析したり、インクリメントしたり、取得元のウォークを超えて永続化したりすることは、すべて契約の範囲外です。カーソルがオフセットではなく使用されるのは、カタログが移動するリストだからです。オフセットによるウォークでは、ウォークの途中でエントリが追加または削除されると、エントリが暗黙的にスキップまたは繰り返され、呼び出し元はそれが発生したことを認識できません。 | +| `limit` | 整数 | 必須 | `minimum: 1`, `maximum: 100` | 実際に提供されたページサイズ。最大値を超える `limit` のリクエストは拒否されず、最大値にクランプされます。そのため、この値はリクエストされた値より小さくなることがあります。ページングには送信した値ではなくこの値を使用してください。そうしないと、実際には受信していない行があると想定してしまうことになります。 |### RouterModelOutput + +パートナーモデルのネイティブなJSON出力ドキュメントであり、呼び出し元にそのまま返されます。その具体的な形状はComfyではなくパートナーが所有しているため、これはオープンなオブジェクトです。Routerはフィールドの絞り込み、名前の変更、再ラップを行いません。これは名前付きコンポーネントです(インラインの匿名オブジェクトではありません)。ComfyUIのスペック駆動のコード生成では、生成対象のクラスが必要になるためです。 + +型: `object`### RouterModelSegment + +正規の `{provider}/{model}[/{variant}]` モデル ID の小文字の `model` セグメント。そのプロバイダー内で実行するモデルを指します。`RouterProviderSegment` と同じくドリフトを防ぐため、呼び出しルートの `model` パスパラメータとカタログエントリの `model` フィールドで共有されます。 + +型: `文字列`。`pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`、`maxLength: 128`### RouterPageCursor + +Router リストに対する**不透明**なカーソルです。これはサーバーによって生成され、常にラウンドトリップされるだけです。オフセットでもなく、モデルIDでもなく、順序付けもされておらず、カタログの再構築をまたいでも安定しません。そのため、カーソルを解析したり、インクリメントしたり、導出元となった走査を超えて永続化したりすることは、すべて契約の範囲外です。カタログが変動するリストであるため、オフセットではなくカーソルが採用されています。オフセットによる走査では、走査の途中でエントリが追加または削除されると、エントリが暗黙的にスキップまたは繰り返され、呼び出し側はその発生を検知できません。 + +型: `string` -- `pattern: ^[A-Za-z0-9._~+/=-]+$`, `minLength: 1`, `maxLength: 512`### RouterProviderSegment + +標準の `{provider}/{model}[/{variant}]` モデルIDの小文字の `provider` セグメント。これは、モデルがアドレス指定されるパートナーを示します。呼び出しルートの `provider` パスパラメータとカタログエントリの `provider` フィールドは、どちらもこの単一のスキーマを参照します。これにより、リストされたIDと受け入れられるIDが乖離するのを防ぎます。 + +型: `string`。`pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`、`maxLength: 64`### RouterValidationErrorContext + +1つの `RouterValidationErrorDetail` に対する違反されたバウンドで、プロバイダーからそのまま引き継がれます。例えば、`greater_than` に付随する `{"limit_value": 8}`、`image_too_small` に付随する `{"min_width": 512}`、`file_too_large` に付随する `{"max_size_bytes": 10485760}` などです。キーのセットはプロバイダーとエラータイプに固有であるため、これは意図的にオープンなオブジェクトです。固定のフィールドリストに絞り込んだり、`msg` 文字列に折り込んだりすることは、移植されたインテグレーションがコンパイルされ、バウンドを読み取るブランチを静かに失う、まさにその方法です。エラータイプがバウンドを持たない場合は存在しません。 + +型: `object`### RouterValidationErrorDetail + +fal/FastAPI 形式のモデルレベルの検証エラー 1 件分です。`type` は、`RouterErrorType` の粗いバケットでは表現できない粒度である、プロバイダー固有の具体的な理由(`value_error`、`missing`、`image_too_small`、`unsupported_audio_format`、`greater_than`、`file_too_large` など)を保持します。同じ理由で、これはオープンな文字列であり、`enum` ではありません。プロバイダーの語彙は 2 つの層にわたって約 48 の値に上り、当社ではなくプロバイダーのリリースサイクルに応じて増えていきます。モデル化されていない値は、デシリアライゼーションに失敗するのではなく、呼び出し元に届かなければなりません。 + +| フィールド | 型 | 必須 | 制約 | 説明 | +| --- | --- | --- | --- | --- | +| `loc` | any の配列 | はい | - | 問題のあるフィールドへのパス。最も外側のセグメントが先頭になります。例えば `["body", "image_url"]`、または整数が配列のインデックスとなる `["body", "images", 0]`。 | +| `msg` | 文字列 | はい | - | この 1 件の失敗を人間が読める形式で説明したもの。 | +| `type` | 文字列 | はい | - | この失敗の具体的かつ機械可読な理由で、プロバイダーから変更されずにそのまま渡されます。型付き SDK の例外階層が分岐する際に参照する値です。レスポンスヘッダーの `error_type` は、その粗いバケットにすぎません。 | +| `ctx` | [`RouterValidationErrorContext`](#routervalidationerrorcontext) | いいえ | - | 1 つの `RouterValidationErrorDetail` について違反された制約で、プロバイダーからそのまま引き継がれます。例えば、`greater_than` の場合は `{"limit_value": 8}`、`image_too_small` の場合は `{"min_width": 512}`、`file_too_large` の場合は `{"max_size_bytes": 10485760}` などです。キーセットはプロバイダーとエラータイプに固有であるため、これは意図的にオープンなオブジェクトです。固定のフィールドリストに絞り込むこと、または `msg` 文字列に折り込むことは、移植された統合がコンパイルに成功した後に、制約を読み取っていた分岐を黙って失う、まさにその方法です。エラータイプが制約を伴わない場合は存在しません。 | +| `input` | [`RouterValidationErrorInput`](#routervalidationerrorinput) | いいえ | - | 問題となった入力値で、呼び出し元が `loc` から再導出しなくても何が拒否されたかを確認できるよう、そのままエコーバックされます。文字列、数値、ブール、配列、オブジェクト、null など、あらゆる JSON 型を取り得るため、このスキーマはオブジェクトに絞り込むのではなく、意図的に型付けされないままとなっています。プロバイダーが入力をエコーバックしない場合は存在しません。 |### RouterValidationErrorInput + +拒否された入力値をそのままエコーバックしたものです。呼び出し元は `loc` から値を再導出しなくても、何が拒否されたかを確認できます。値はあらゆるJSON型(文字列、数値、ブール、配列、オブジェクト、null)を取り得るため、このスキーマはオブジェクトに限定せず、意図的に型指定なしとしています。プロバイダーが入力をエコーバックしない場合、このフィールドは存在しません。### RouterValidationErrorResponse + +Routerのモデルレベルの`422`ボディ(fal/FastAPI形式):リクエストはモデルに到達するのに十分な形式であり、モデルがその内容を拒否したことを示します。これ自体には`error_type`が含まれないことに注意してください。その役割はレスポンスの`X-Comfy-Error-Type`ヘッダーが担うため、クライアントは2種類のRouterエラーボディのどちらを受信したかを先に判断することなく、ヘッダーから大まかな分類を読み取ることができます。 + +| フィールド | 型 | 必須 | 制約 | 説明 | +| --- | --- | --- | --- | --- | +| `detail` | [`RouterValidationErrorDetail`](#routervalidationerrordetail)の配列 | はい | - | リクエストで見つかったすべての検証エラー。問題のあるフィールドごとに1つのエントリ。 | diff --git a/ja/api-reference/v2/overview.mdx b/ja/api-reference/v2/overview.mdx index fa109a38a..1103266e3 100644 --- a/ja/api-reference/v2/overview.mdx +++ b/ja/api-reference/v2/overview.mdx @@ -1,7 +1,7 @@ --- title: "Comfy API v2 の概要" description: "公式 Comfy API v2 リファレンス:入力をアップロードしてワークフローを送信し、結果をポーリングして外部アプリケーションから ComfyUI ワークフローを実行します。" -translationSourceHash: c3d19ef0 +translationSourceHash: 0a5bed89 translationFrom: api-reference/v2/overview.mdx --- @@ -35,3 +35,7 @@ translationFrom: api-reference/v2/overview.mdx |----------|-------------| | アセット | コンテンツアドレス型 blob 上の UUID 識別レコード。入力のアップロードと出力のダウンロードを行います。 | | ジョブ | ワークフローの 1 回の実行。永続的で、ポーリング可能であり、キャンセルも可能です。 | + +## Comfy Router + +Comfy API v2 は、送信してポーリングする永続的なジョブとしてワークフローを実行します。モデルを直接呼び出す場合(パートナーモデル 1 つ、リクエスト 1 つ、モデル固有のネイティブな入出力)は、代わりに [Comfy Router](/ja/api-reference/comfy-router/quickstart) をご覧ください。 diff --git a/ko/api-reference/comfy-router/limitations.mdx b/ko/api-reference/comfy-router/limitations.mdx new file mode 100644 index 000000000..f5fe0a5e5 --- /dev/null +++ b/ko/api-reference/comfy-router/limitations.mdx @@ -0,0 +1,111 @@ +--- +title: "Comfy Router 제한 사항" +description: "현재 Comfy Router가 지원하지 않는 기능, 대안이 존재하는 경우 대신 사용할 도구, 그리고 변경될 것으로 예상되는 제한 사항을 설명합니다." +translationSourceHash: ebed2b5e +translationFrom: api-reference/comfy-router/limitations.mdx +translationBlockHashes: + "_intro": c72d3766 + "At a glance": dd3cde31 + "No queued submission": f24e2b9d + "No cost or credit figures on a response": 0d8505a9 + "No progress while a call runs": 40667680 + "Three forecast buckets are not in the vocabulary": 6f256b4e + "Router does not cover every partner operation": 069b148a + "Next": c839d9e8 +--- + +**Comfy Router는 아직 일반에 공개되지 않았습니다.** 아래에서 언급하는 라우트(`POST /v1/models/{provider}/{model}` 및 카탈로그·스키마 관련 라우트)는 아직 요청을 처리하지 않습니다. 현재 인증된 호출은 `404`를 반환합니다. 이 페이지는 해당 라우트들이 제공할 계약을 설명하며, 통합이 알려진 형태를 기준으로 작성될 수 있도록 해당 롤아웃 이전에 게시되었습니다. 아래의 모든 내용은 해당 계약에 대한 설명이지, 지금 당장 실행할 수 있는 동작에 대한 설명이 아닙니다. + + +Comfy Router는 하나의 동기식 호출입니다. 파트너 모델의 네이티브 입력을 하나의 자격 증명으로 하나의 호스트에 전송하면 연결이 유지되고, `200` 응답이 해당 모델의 네이티브 출력을 전달합니다. 이러한 형태 덕분에 첫 번째 통합이 짧아지며, 이 페이지의 모든 제한 사항도 바로 여기서 비롯됩니다. Router를 중심으로 설계하기 전에, 설계 이후가 아니라 이 페이지를 읽으십시오. 아래 내용의 대부분은 간단한 대안이 있으며, 그렇지 않은 항목은 Router에 대해 성립하지 않는 가정을 기반으로 구축하기 전에 알아둘 가치가 있습니다. + +## 한눈에 보기 + +각 행은 해당 제한을 설명하는 섹션으로 연결됩니다. **의도적**은 해당 제한이 Router 작동 방식의 일부이며 어떤 것도 기다리지 않는다는 뜻이고, **아직**은 Router가 해당 기능을 갖출 것으로 예상되지만 이 페이지는 시기에 대한 약정을 하지 않는다는 뜻입니다. + +| 제한 사항 | 대신 사용할 방법 | 상태 | +| --- | --- | --- | +| [대기 중 제출 없음: 호출은 동기식](#대기-중-제출-없음) | 연결을 계속 열어 두거나, 제출과 폴링을 수행하는 파트너 프록시 라우트를 사용하세요. | 아직 | +| [응답에 비용 또는 크레딧 수치 없음](#응답에-비용-또는-크레딧-수치가-없음) | Comfy 플랫폼에서 잔액과 사용량을 확인하고, 호출 이전에 모델 카탈로그 항목의 `billing`을 확인하세요. | 아직 | +| [손실된 호출을 재개할 방법 없음](#no-way-to-resume-a-call-you-lost) | `Idempotency-Key`를 보내면 재시도가 최대 한 번만 청구됩니다. 단, 손실된 결과를 복구하지는 않습니다. | 아직 | +| [호출이 서버 마감 시간에 중단됨](#calls-are-cut-off-at-a-server-deadline) | 클라이언트에 마감 시간보다 긴 타임아웃을 설정하고, 마감 시간 안에 완료할 수 없는 작업은 분할하세요. | 의도적 | +| [호출 실행 중 진행 상황 없음](#호출이-실행되는-동안에는-진행률이-없음) | 현재 Router에는 해당 기능이 없습니다. 파트너 프록시 라우트가 자체 진행 상황을 노출할 수 있습니다. | 아직 | +| [세 가지 예측 버킷은 어휘에 포함되지 않음](#세-가지-예고된-버킷은-어휘에-포함되어-있지-않습니다) | Router가 게시하는 열다섯 가지 버킷을 처리하고, 인식할 수 없는 값은 `internal_error`로 처리하세요. | 아직 | +| [Router가 모든 파트너 작업을 다루지는 않음](#router는-모든-파트너-작업을-다루지-않습니다) | 동일 호스트의 `/proxy/…` 하위 파트너 프록시 라우트를 사용하세요. | 의도적 | + +## 대기 중 제출 없음 + +모델을 실행하는 방법은 하나뿐입니다. `POST /v1/models/{provider}/{model}`은 생성이 끝날 때까지 연결을 유지한 뒤 응답으로 결과를 반환합니다. 작업(job)을 받아 식별자를 돌려주고 나중에 결과를 가져갈 수 있게 해주는 엔드포인트는 없으며, 완료를 알리는 콜백이나 웹훅도 없습니다. 대기 중 제출에 해당하는 엔드포인트가 계획되어 있고 API 레퍼런스에는 `/v1/queue/models/{provider}/{model}`로 언급되어 있지만, 현재는 API 계약의 일부가 아니며 해당 경로로의 호출은 처리되지 않습니다. + +**대신 할 수 있는 방법.** 대부분의 모델에서는 이는 문제가 되지 않습니다. 연결을 유지한 채 결과를 읽으면 됩니다. 빠른 이미지 모델은 몇 초 안에 결과를 반환하고, 긴 비디오 생성은 수 분 동안 실행될 수 있지만 Router가 그동안 연결을 유지합니다. 클라이언트 읽기 타임아웃을 넉넉하게, [Router 자체의 데드라인](#calls-are-cut-off-at-a-server-deadline) 이상으로 설정하고, 이 호출을 빠른 요청이 아닌 장기 실행으로 취급하세요. 아키텍처상 정말로 연결을 유지할 수 없는 경우, 즉 실행 시간 상한이 짧은 서버리스 함수나 사용자가 닫을 것으로 예상되는 브라우저 탭이라면, 연결을 유지할 수 있는 여러분이 제어하는 워커에서 호출을 실행하거나, 자체 제출(submit) 및 폴링(poll) 쌍을 제공하는 공급자의 파트너 프록시 경로를 사용하세요. [마지막 섹션](#router는-모든-파트너-작업을-다루지-않습니다)을 참조하세요. + +**상태: 아직 없음.** 대기 중 경로는 제공될 예정이지만, 이 페이지에서는 언제 제공될지 약속하지 않습니다. + +## 응답에 비용 또는 크레딧 수치가 없음 + +Router 응답은 모델이 무엇을 생성했는지 알려주며, 그 계약에는 비용이 얼마인지에 대한 정보가 전혀 없습니다. 응답 본문에는 청구 금액, 크레딧 잔액, 사용량 수치가 없으며, 해당 라우트는 비용 헤더를 선언하지 않습니다. 놀라지 않도록 한 가지 주의할 점을 말씀드립니다. Router는 파트너 프록시 라우트와 청구 경로를 공유하며, 해당 경로는 허용 목록에 포함된 공급자의 청구된 응답에 `X-Comfy-Credits-Used`를 기록합니다. 따라서 그중 하나를 대상으로 한 Router 호출에는 이 헤더가 나타날 수 있습니다. 이는 Router 계약의 일부가 아닙니다. 허용 목록에 없는 모든 공급자에게는 이 헤더가 없으며, 멱등 재시도(idempotent retry) 시 의도적으로 *재생되지 않습니다*. 이는 정확히, 이 헤더를 합산하는 클라이언트가 한 번만 결제된 호출을 이중으로 계산할 수 없게 하기 위해서입니다. 이 헤더를 기반으로 정산을 구축하지 마십시오. 모델 카탈로그도 마찬가지입니다. 카탈로그에는 호출자가 호출 이전에 필요한 청구 관련 *사실*만 담겨 있으며 가격은 결코 포함되지 않습니다. 따라서 Router 응답만으로 지출을 정산할 수 없으며, X를 다른 곳에서 가져오지 않고는 사용자에게 "이 호출 비용은 X입니다"라고 보여줄 수 없습니다. + +**대신 이렇게 하세요.** 잔액, 사용량, 청구서는 [platform.comfy.org](https://platform.comfy.org)의 Comfy 플랫폼에 있습니다. 이곳이 지출한 금액과 남은 금액에 대한 소스(source of truth)이며, 이 페이지의 어떤 내용에도 영향을 받지 않습니다. Router가 호출 시점에 알려주는 두 가지는 활용할 가치가 있습니다. 크레딧 부족으로 거부된 호출은 `insufficient_credits`로 반환되므로, 잔액을 사전 확인하는 대신 크레딧 소진을 유형화된 오류로 처리할 수 있습니다. 그리고 각 모델의 카탈로그 항목에는 `billing.charges_on_policy_rejection`이 포함되어 있는데, 이는 해당 특정 모델이 콘텐츠 정책상 이유로 거부하는 생성에 대해 비용을 청구하는지 여부를 알려줍니다. 이 필드는 논리값이 아니라 **세 가지 값을 가진 문자열**입니다. `yes`, `no`, `unknown`입니다. `unknown`은 "청구될 수 있음"으로 해석하십시오. 즉, 아직 아무도 해당 모델의 동작을 확정하지 못했다는 뜻이며, 이 값은 확인되지 않은 모델이 `no`로 게시되지 않도록 하기 위해 정확히 존재합니다. `no`는 주장이기 때문입니다. 이 필드는 의도적으로 `enum`이 아니므로, 인식하지 못하는 값도 `unknown`으로 취급하고, 이 값에 대해 truthiness 검사를 작성하지 마십시오. 문자열 `"no"`는 대부분의 언어에서 truthy이며, 그런 검사는 이 필드가 잡아내기 위해 존재하는 바로 그 경우를 반대로 만들어 버립니다. 공급자마다 이 부분이 다르며, 그 차이는 호출 시점에는 보이지 않습니다. 호출 이전에 이 값을 읽는 것이 나중에 설명할 수 없는 청구를 피하는 방법입니다. + +**상태: 아직 아님.** 호출별 수치는 아직 지원되지 않습니다. 참고로 *카탈로그*에는 의도적으로 가격이 포함되지 않습니다. 가격은 가격이 유지 관리되는 곳에 속하며, 그 가격과 점점 어긋나게 될(drift) 모델 목록에 중복으로 복사되지 않아야 합니다. + +## No way to resume a call you lost + +Router does not keep a resumable record of an in-flight call. There is no status route, no job identifier, and nothing to reconnect to: if the connection drops mid-call — a client crash, a network partition, a deploy that restarts your process — the response is gone, and the call is not something you can ask about afterwards. Whether the *generation* completed and was charged is a separate question from whether you received it, and losing the connection does not reliably answer either. + +**What to do instead.** Send an `Idempotency-Key` header on every call. It does not make a lost call resumable, but it makes retrying one safe. Router reserves the key for the duration of the call, and when the call actually reached you with an answer it records that response against the key for 24 hours; retrying with the **same** key then replays the recorded response instead of dispatching — and re-charging — the provider a second time, marked `Idempotent-Replayed: true` so you can tell a replay from a fresh run. Generate a fresh key per logical call, not per attempt; the same key presented with a *different* request body is a `409` rather than a silent overwrite. + +Be precise about what that buys you, because it is a **billing** property and not a delivery one: **a key is charged at most once.** It is not a promise that a key is dispatched to the provider at most once. Router holds a key against an answer you actually received; the outcomes that charged you nothing release it so the call can be made again. A `5xx`, a `408`/`425`/`429`, and — this is the one that matters here — a call where nothing reached you at all: each of those releases the key, and a retry with it genuinely re-runs and re-dispatches the provider. + +**So a dropped connection is the case idempotency does *not* rescue.** A connection lost mid-call usually means no response was ever committed to you, which is exactly the release path above: retrying with the same key starts a fresh run rather than handing you the result you missed, and if the original generation had already been dispatched the provider may run it a second time. That is the right default — an unbilled call you never received should be re-runnable — but plan for "retry produces a new run", not "retry collects the lost one". + +When Router *does* hold something for the key, the retry is answered rather than re-run: either the original response replayed, or a `409` explaining why it cannot be. A retry sent while the original is still in flight is a `409` carrying `Retry-After`, so wait and re-send the same key. A retry against a call that completed but whose response Router could not keep a faithful copy of is also a `409` — and that is not only the oversized-response case: a response past the replay cap, a handler that failed or panicked after answering, and a write to you that failed or came up short all record the key as consumed-but-not-replayable and return the same `409`. Do not go hunting for a size problem when you see it. The guidance in every one of those cases is the same: use a **new** key. The original completed and was charged, and Router will neither invent its response nor re-run it under the old key. + + +**Not yet in the generated contract.** The `Idempotency-Key` request header, the `409` response and the `Idempotent-Replayed` and `Retry-After` response headers described here are not declared on `POST /v1/models/{provider}/{model}` in the OpenAPI contract the reference is generated from, so they do not appear in the generated API reference and the SDKs do not model them. Send and read them yourself until they do. + + +**Status: not yet.** Durable, resumable execution is expected to arrive with the queued path, which is where a request record has somewhere to live. Idempotent retry is the answer today and is not a stopgap — it is worth wiring in regardless. + +## Calls are cut off at a server deadline + +One Router call may hold its connection for **10 minutes**. That is the default; it is a server-side configuration value rather than a fixed constant, so treat it as the number to design against rather than a guarantee etched into the contract. Past it, Router stops waiting, cancels its own in-flight request to the provider and answers `504` with `X-Comfy-Error-Type: deadline_exceeded`. **A `deadline_exceeded` call is not billed** — the bound is ours, so its cost is ours. + +Two things that cancellation does not do, both worth knowing before you retry. It does not recall a generation a provider has already accepted: for the partners Router drives by submitting a job and polling it, expiring the deadline ends Router's own wait, not the provider's work, so that job can run to completion and a retry can produce a **second generation** (you are still not billed for the timed-out call). And it cannot un-send an answer: if the handler wins the race and commits a response just as the bound expires, you keep that response rather than the `504`. + +Do not confuse it with the other `504`. `provider_timeout` is the partner failing to answer in time, and that one **is** billed; `deadline_exceeded` is Router's own bound expiring. Two causes, two billing outcomes, which is exactly why they are two buckets on the same status code — branch on `X-Comfy-Error-Type`, never on the status alone. + +**What to do instead.** Set your client's read timeout comfortably *above* the deadline, not below it. A client that gives up first turns a typed `504` with a request identifier into an opaque local abort, and you lose the one artifact support can trace. If a single generation genuinely cannot finish inside the deadline, Router is not the right shape for it today: run it through a partner-proxy route that submits and polls, or break the work into calls that each finish inside the bound. + +**Status: deliberate.** A bound has to exist — without one, a stuck upstream holds a connection and a concurrency slot indefinitely. The specific number may be tuned; the existence of a deadline will not go away. + +## 호출이 실행되는 동안에는 진행률이 없음 + +`POST /v1/models/{provider}/{model}`는 종료 시점에 정확히 한 번만 응답을 반환합니다. 스트리밍 응답, 서버 전송 이벤트, 백분율, 부분 또는 미리보기 프레임이 없습니다. 이는 파트너의 자체 API가 제출 후 폴링 방식인 경우에도 마찬가지입니다. Router는 해당 폴링을 내부적으로, 즉 여러분의 단일 호출 안에서 처리하며, 그 과정에서 확인되는 중간 상태는 여러분에게 전달되지 않습니다. 외부에서 보면 3초짜리 이미지와 6분짜리 비디오는 같은 형태입니다. 요청 하나, 응답 하나, 그 사이에 아무것도 없습니다. + +**대신 해야 할 일.** 현재 Router에서는 할 수 있는 일이 없습니다. 출처를 알 수 없는 백분율 대신 불확정 진행 상태를 표시하세요. 특정 공급자에게 진행률이 필수 요구 사항이라면, 해당 공급자의 파트너 프록시 라우트가 자체 폴링이나 스트리밍을 제공하는지 확인하고 그 라우트를 직접 사용하세요. 일부는 해당 기능을 제공하며, 그 라우트는 변경되지 않았고 완전히 지원됩니다. + +**상태: 아직 미지원**이며 대기 중 경로와 연결되어 있습니다. 진행률은 보고할 *곳*이 필요합니다. 대기 중 제출은 그 대상을 제공하지만 단일 동기 호출은 제공하지 않습니다. + +## 세 가지 예고된 버킷은 어휘에 포함되어 있지 않습니다 + +Router의 `error_type` 어휘는 **15개로 이루어진 닫힌 집합**이며, [API 레퍼런스](/ko/api-reference/comfy-router/reference)에 나열되고 quickstart가 가리키는 바로 그 15개입니다. 해당 레퍼런스의 본문에는 예상 추가 항목으로 세 가지가 더 언급됩니다: `file_download_error`, `cancelled`, `queue_timeout`. 이름만 언급되었을 뿐이며, 그게 전부입니다. 이들은 오늘날 **집합의 멤버가 아닙니다**: 어떤 Router 응답도 이들을 담지 않으며, 계약(contract)으로부터 생성된 클라이언트는 이들을 알지 못하고, Router가 내부적으로 이들을 넘겨받더라도 전송하는 대신 `internal_error`로 대체합니다. 따라서 오늘 이들을 위해 작성하는 분기는 결코 실행되지 않는 분기이며, 레퍼런스에 이들이 등장한다고 해서 Router가 호출을 취소하거나 큐에 넣는다는 증거가 되지는 않습니다. Router는 그중 어느 것도 하지 않습니다. + +이들이 아예 빠지지 않고 문서로 예고된 이유는 `error_type`이 의도적으로 단순한 문자열이지 `enum`이 아니기 때문이며, 인식할 수 없는 버킷을 무조건 거부하는 클라이언트는 이미 무언가 잘못된 바로 그 순간에 가장 크게 실패하기 때문입니다. 추가 항목을 미리 이름으로 언급하는 것은, 이 집합이 의도적으로 개방형이라는 것을 독자가 알 수 있게 하는 방법입니다. + +**대신 해야 할 일.** Router가 실제로 게시하는 15개 버킷을 처리하세요. 전체 목록은 [API 레퍼런스](/ko/api-reference/comfy-router/reference)에 있습니다. 그리고 인식할 수 없는 모든 값을 `internal_error`로 취급하는 폴백 분기를 하나 작성하세요. 그 폴백이 바로 전체 메커니즘입니다. 이 폴백 덕분에 이 세 가지와, 클라이언트가 작성된 이후에 추가되는 어떤 버킷이든 여러분을 깨뜨리지 않고 도착할 수 있습니다. 제어 흐름을 위해서는 대략적인 버킷을 기준으로 분기하고, 구체적인 이유가 필요할 때는 `422` 본문 안의 필드별 `type`을 읽으세요. + +**상태: 아직 아님.** 세 가지 각각은 Router가 아직 갖추지 못한 동작에 해당하며, 각각은 그것을 내보내기 시작하는 바로 그 변경과 함께 어휘에 합류합니다. 절대 그 이전에는 합류하지 않습니다. + +## Router는 모든 파트너 작업을 다루지 않습니다 + +Router는 파트너 *모델*을 실행합니다. 파트너가 노출하는 모든 작업을 중계하지는 않습니다. 파일 업로드, 계정 및 에셋 읽기, 공급자별 관리 호출, 스트리밍 채팅 엔드포인트, 일부 파트너가 게시하는 제출-폴링(submit-and-poll) 쌍이 여기에 해당합니다. 또한 Router는 이러한 작업을 변형하지도 않습니다. 모델의 네이티브 입력을 전달하고 네이티브 출력을 변경 없이 반환하므로, 지원되지 않는 작업을 이식할 수 있는 통합 봉투가 없습니다. + +**대신 수행할 작업.** `/proxy/…` 아래의 파트너 프록시 라우트는 동일한 호스트에서 동일한 자격 증명으로 완전히 지원되며, Router가 다루지 않는 모든 작업에 대한 해답입니다. 이들은 지원 중단되지 않았고, 서비스 종료(sunset) 경로에 있지도 않습니다. 동일한 통합에서 Router와 함께 사용하는 것은 우회 방법이 아니라 예상된 사용 방식입니다. 여러 모델에 걸쳐 하나의 라우트 형태와 하나의 자격 증명을 원한다면 Router를 사용하고, 특정 파트너 작업, 공급자 자체의 스트리밍 응답, 또는 Router가 의도적으로 숨기는 제출-폴링 제어가 필요하다면 `/proxy/…`를 사용하세요. + +**상태: 의도된 설계입니다.** Router는 의도적으로 표면을 좁힙니다. 하나의 라우트 형태가 바로 기능입니다. 프록시 표면은 기존 그대로 유지됩니다. + +## 다음 + +- [Comfy Router 빠른 시작](/ko/api-reference/comfy-router/quickstart): Python 또는 TypeScript로 첫 번째로 동작하는 호출을 만들어 봅니다. +- [Comfy Router API 참조](/ko/api-reference/comfy-router/reference): 모든 엔드포인트, 모든 매개변수, 그리고 Router가 전송하는 모든 오류 버킷을 다룹니다. diff --git a/ko/api-reference/comfy-router/quickstart.mdx b/ko/api-reference/comfy-router/quickstart.mdx new file mode 100644 index 000000000..e5f4e9bf8 --- /dev/null +++ b/ko/api-reference/comfy-router/quickstart.mdx @@ -0,0 +1,308 @@ +--- +title: "Comfy Router 빠른 시작" +description: "아무것도 없는 상태에서 Python과 TypeScript로 Comfy Router를 사용해 약 5분 만에 생성된 이미지를 얻는 방법." +translationSourceHash: 1825ee75 +translationFrom: api-reference/comfy-router/quickstart.mdx +translationBlockHashes: + "_intro": fd4302ce + "Why this page uses `bfl/flux-2-pro`": 04e30793 + "Get a key": 447aa37e + "cURL": bc3e1e4c + "Python": 059e95b8 + "TypeScript": fed849f1 + "Reading the `422`": 6d59c614 + "Where the model's fields come from": 157d882f + "Next": ce5a29fb +--- + +**Comfy Router는 아직 일반에 공개되지 않았습니다.** 아래의 라우트, 즉 `POST /v1/models/{provider}/{model}` 및 해당 카탈로그와 스키마 관련 라우트는 아직 요청을 처리하지 않습니다. 현재 인증된 호출은 `404`를 반환합니다. 이 페이지는 이 라우트가 제공할 계약을 문서화하며, 해당 출시에 앞서 게시되어 통합 코드를 미리 작성할 수 있도록 합니다. 지금 바로 사용할 수 있는 동작에 대한 설명은 아닙니다. + + +Comfy Router는 파트너 모델을 하나의 호스트, 하나의 자격 증명, 하나의 라우트 형태 뒤에서 실행합니다. 이 페이지는 생성된 이미지에 도달하는 가장 짧은 완전한 경로입니다. 클라이언트를 설치하고, 키를 설정하고, 요청을 하나 보내고, 결과를 읽고, 실제로 마주하기 이전에 첫 번째 실패가 어떤 모습인지 확인하는 것입니다. + +Base URL은 `https://api.comfy.org`입니다. 라우트는 `POST /v1/models/{provider}/{model}`이며, 요청 본문은 모델 자체의 네이티브 JSON 입력이고, `200`은 모델 자체의 네이티브 JSON 출력을 전달합니다. Router는 입력도 출력도 래핑하지 않으므로, 이미 파트너 API에 대해 작성한 호출은 호스트만 변경하면 Router 호출이 됩니다. + +## 이 페이지에서 `bfl/flux-2-pro`를 사용하는 이유 + +`bfl/flux-2-pro`는 p50 기준 약 3.1초 만에 결과를 반환하며, 이는 Router에서 측정된 경로 중 가장 빠른 것입니다. 바로 이 점 덕분에 5분 안에 첫 결과를 얻는 것이 현실적입니다. 더 느린 모델을 사용한다면 그 시간을 문서를 읽는 대신 기다리는 데 쓰게 될 것입니다. + +이는 편의를 위한 것이지 필수 사항은 아닙니다. Router의 다른 모든 모델도 정확히 동일한 방식으로 호출됩니다. 동일한 라우트, 동일한 자격 증명 헤더, 동일한 오류 범주, 동일한 `X-Comfy-Request-Id`를 사용합니다. 변경되는 것은 모델 ID, 요청 본문 내부의 필드, 그리고 반환되는 결과의 형태뿐입니다. 예를 들어 Gemini는 p95 기준 72.8초로 여유 있게 완료됩니다. Router는 폴링할 작업 핸들을 반환하는 대신 전체 생성 과정 동안 연결을 유지합니다. 긴 호출을 중도에 끊는 엣지 상한선은 없지만, Router는 호출 자체에 한계를 둡니다. 자체 서버 데드라인(기본 10분)이 연결을 유지하는 최대 시간이며, 이를 초과하면 `504` / `deadline_exceeded`로 응답하고 청구하지 않습니다. 모델 ID를 교체하고 해당 모델의 필드를 자체 스키마(아래)에서 읽으면 됩니다. + +## 키 발급받기 + +Router는 Comfy API 키로 인증합니다. [platform.comfy.org/profile/api-keys](https://platform.comfy.org/profile/api-keys)에서 키를 생성한 다음 환경 변수에 넣으세요. 아래 두 샘플 모두 `COMFY_API_KEY`를 읽으며 키를 리터럴로 받지 않으므로, 복사해서 붙여넣은 스니펫에는 자격 증명이 포함되지 않아 커밋에 올라갈 일이 없습니다. + +```bash +export COMFY_API_KEY="comfyui-..." +``` + + +`comfyui-` 키는 `Authorization: Bearer`가 아닌 **`X-API-Key`** 헤더로 보내세요. +두 헤더는 서로 다른 검증기를 선택합니다. `X-API-Key`는 `comfyui-` 키를 읽는 유일한 인바운드 +검증기이며, `Authorization`에 담긴 값은 JWT 분기로 라우팅되어 JWT가 아닌 토큰은 +`401 Invalid token`으로 종료되고 키는 결코 조회되지 않습니다. (`Authorization: Bearer`는 +Cloud/Firebase **JWT**에 올바른 방식입니다. 생성된 +[API reference](/ko/api-reference/comfy-router/reference)에서 "bearer token"이 의미하는 바가 바로 이것입니다.) + + +키는 워크스페이스별로 존재하며 해당 워크스페이스의 모델 사용 권한과 크레딧 잔액을 수반합니다. 사용 가능한 자격 증명이 없는 요청은 `X-Comfy-Error-Type: unauthorized`와 함께 `401`을 반환하고, 워크스페이스가 모델을 실행할 수 없는 요청은 `403` / `forbidden`을 반환합니다. + +## cURL + +가장 짧은 호출로, 스크립트, 스모크 테스트, 터미널에 복사하여 붙여넣기용입니다: + +```bash +curl https://api.comfy.org/v1/models/bfl/flux-2-pro \ + -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"}' +``` + +응답은 모델의 네이티브 출력이며, 아래 샘플들이 읽는 것과 정확히 동일합니다. 실패 시 본문에는 오류가 포함되고 `X-Comfy-Error-Type` 헤더가 오류 범주를 명명합니다. 나중에 문의해야 하는 응답의 `X-Comfy-Request-Id` 헤더를 보관하세요. macOS와 Linux에는 `uuidgen`이 기본 제공됩니다. Windows에서는 `New-Guid` 또는 다른 UUID 소스로 Idempotency-Key를 생성하세요. + +## Python + +Requires Python 3.9+ and `httpx`: + +```bash +pip install httpx +``` + +Save as `quickstart.py` and run it with `python quickstart.py`: + +```python +import os +import uuid + +import httpx + +BASE_URL = os.environ.get("COMFY_ROUTER_BASE_URL", "https://api.comfy.org") +MODEL = "bfl/flux-2-pro" + +# Give the client headroom ABOVE Router's own server deadline (10 minutes by +# default) so a call that reaches the server bound comes back as a typed 504 +# with a request id rather than as an opaque client abort. The deadline bounds +# how long Router holds the connection, not whether the call is billed: if the +# provider completed the generation, it is billed either way. +READ_TIMEOUT_SECONDS = 660.0 + + +class RouterError(Exception): + """A Comfy Router failure, typed by its X-Comfy-Error-Type bucket.""" + + def __init__(self, response: httpx.Response) -> None: + self.error_type = response.headers.get("X-Comfy-Error-Type", "internal_error") + self.request_id = response.headers.get("X-Comfy-Request-Id") + self.status_code = response.status_code + # Parse defensively: an error can arrive as an HTML 502 from a load + # balancer, a plain-text 429, an empty body or a truncated JSON one. The + # status, the bucket and the request id above are the parts worth + # keeping, so a body that will not parse must not replace this exception + # with a JSONDecodeError and lose them. + body = None + if response.headers.get("content-type", "").startswith("application/json"): + try: + body = response.json() + except ValueError: + body = None + detail = body.get("detail") if isinstance(body, dict) else None + # A 422 carries a detail[] array - one entry per rejected field, each + # keeping its own `loc`, `msg` and `type`. Every other bucket carries a + # plain `detail` string. + self.errors = detail if isinstance(detail, list) else [] + self.detail = detail if isinstance(detail, str) else f"HTTP {response.status_code}" + super().__init__(self.detail) + + +def run(model: str, arguments: dict, idempotency_key: str) -> dict: + # Idempotency-Key makes a retry safe on a PAID call: Router replays the + # original response for 24h instead of dispatching (and billing) the + # provider a second time. Reuse the SAME key when retrying one logical + # call; generate a new one for a new call. + response = httpx.post( + f"{BASE_URL}/v1/models/{model}", + headers={ + "X-API-Key": os.environ["COMFY_API_KEY"], + "Idempotency-Key": idempotency_key, + }, + json=arguments, + timeout=httpx.Timeout(READ_TIMEOUT_SECONDS, connect=10.0), + ) + if response.is_error: + raise RouterError(response) + return response.json() + + +result = run( + MODEL, + {"prompt": "a red teapot on a windowsill, morning light"}, + idempotency_key=str(uuid.uuid4()), +) +# Router forwards each provider's native output unchanged, so this path is +# BFL's, not a Router envelope. Reading a different model means reading its own +# output shape. +print("image:", result["result"]["sample"]) + +# The first failure most callers hit: a field the model's input schema requires +# is missing, so Router rejects the request BEFORE any provider call - which is +# why a 422 is never billed. +try: + run(MODEL, {"width": 1024}, idempotency_key=str(uuid.uuid4())) +except RouterError as exc: + print(f"{exc.error_type} (HTTP {exc.status_code}), request id {exc.request_id}") + for entry in exc.errors: + print(" ", ".".join(str(p) for p in entry["loc"]), "->", entry["msg"]) +``` + +```text +image: https://.../out.jpeg +invalid_input (HTTP 422), request id 6f1c... + body.prompt -> Field required +``` + +## TypeScript + +Requires Node 18+ (for built-in `fetch`, `AbortSignal.timeout` and `crypto.randomUUID`) and `tsx` to run TypeScript directly: + +```bash +npm install --save-dev tsx +``` + +Save as `quickstart.mts` — the `.mts` extension is load-bearing, because the file uses top-level `await` and that needs an ES module — and run it with `npx tsx quickstart.mts`: + +```typescript +const BASE_URL = process.env.COMFY_ROUTER_BASE_URL ?? "https://api.comfy.org"; +const MODEL = "bfl/flux-2-pro"; + +// Headroom ABOVE Router's own server deadline (10 minutes by default), so a +// call that reaches the server bound returns a typed 504 with a request id +// rather than aborting locally at the same moment. The deadline bounds how +// long Router holds the connection, not whether the call is billed: if the +// provider completed the generation, it is billed either way. +const CLIENT_TIMEOUT_MS = 660_000; + +interface ValidationEntry { + loc: (string | number)[]; + msg: string; + type: string; +} + +/** A Comfy Router failure, typed by its `X-Comfy-Error-Type` bucket. */ +class RouterError extends Error { + readonly errorType: string; + readonly requestId: string | null; + readonly status: number; + /** A 422 carries a `detail[]` array — one entry per rejected field, each + * keeping its own `loc`, `msg` and `type`. Every other bucket carries a + * plain `detail` string. */ + readonly errors: ValidationEntry[]; + + constructor(response: Response, body: unknown) { + const detail = + typeof body === "object" && body !== null + ? (body as { detail?: unknown }).detail + : undefined; + super(typeof detail === "string" ? detail : `HTTP ${String(response.status)}`); + this.name = "RouterError"; + this.errorType = response.headers.get("X-Comfy-Error-Type") ?? "internal_error"; + this.requestId = response.headers.get("X-Comfy-Request-Id"); + this.status = response.status; + this.errors = Array.isArray(detail) ? (detail as ValidationEntry[]) : []; + } +} + +/** Read a body without letting a non-JSON error page mask the real failure. */ +async function parseBody(response: Response): Promise { + const text = await response.text(); + try { + return JSON.parse(text) as unknown; + } catch { + return undefined; + } +} + +async function run( + model: string, + args: Record, + idempotencyKey: string, +): Promise { + // Idempotency-Key makes a retry safe on a PAID call: Router replays the + // original response for 24h instead of dispatching (and billing) the provider + // a second time. Reuse the SAME key when retrying one logical call. + const response = await fetch(`${BASE_URL}/v1/models/${model}`, { + method: "POST", + headers: { + "X-API-Key": process.env.COMFY_API_KEY ?? "", + "Idempotency-Key": idempotencyKey, + "Content-Type": "application/json", + }, + body: JSON.stringify(args), + signal: AbortSignal.timeout(CLIENT_TIMEOUT_MS), + }); + // Branch on `ok` FIRST: an HTML 502, a plain-text 429 or an empty body must + // still surface the status, the bucket and the request id. + const body = await parseBody(response); + if (!response.ok) throw new RouterError(response, body); + return body as T; +} + +const result = await run<{ result: { sample: string } }>( + MODEL, + { prompt: "a red teapot on a windowsill, morning light" }, + crypto.randomUUID(), +); +// Router forwards each provider's native output unchanged, so this path is +// BFL's, not a Router envelope. Reading a different model means reading its own +// output shape. +console.log("image:", result.result.sample); + +// The first failure most callers hit: a field the model's input schema requires +// is missing, so Router rejects the request BEFORE any provider call - which is +// why a 422 is never billed. +try { + await run(MODEL, { width: 1024 }, crypto.randomUUID()); +} catch (exc) { + if (!(exc instanceof RouterError)) throw exc; + console.log(`${exc.errorType} (HTTP ${String(exc.status)}), request id ${String(exc.requestId)}`); + for (const entry of exc.errors) console.log(" ", entry.loc.join("."), "->", entry.msg); +} +``` + +```text +image: https://.../out.jpeg +invalid_input (HTTP 422), request id 6f1c... + body.prompt -> Field required +``` + +## `422` 읽기 + +`422`는 첫 실제 호출 이전에 이해할 가치가 있는 유일한 오류입니다. 바로 사용자가 발생시키는 오류이기 때문입니다. Router가 요청 본문을 모델 자체의 입력 스키마와 대조한 후 거부했음을 의미합니다. 필수 필드 누락, 범위를 벗어난 값, 너무 작은 이미지 등이 그 예입니다. + +이 검사는 모든 공급자 호출 이전에 실행되므로 `422`는 비용이 들지 않습니다. 파트너 지출도 없고, 이후에 답변해야 할 청구 문제도 없습니다. 이는 필드별 실패가 아닌 요청 수준 실패(잘못된 커서, 읽을 수 없는 봉투)인 `400`과는 다릅니다. + +그 본문은 fal/FastAPI의 `detail[]` 형태입니다. 문제가 있는 각 필드마다 항목이 하나씩 있는 배열이며, 각 항목은 자체 `loc`(필드 경로), `msg`, `type`(공급자 수준의 구체적인 이유: `missing`, `value_error`, `image_too_small`) 및 이유에 경계값이 포함된 경우 `ctx`를 유지합니다. 이러한 필드별 세분성 때문에 위 샘플들은 배열을 예외 메시지로 평탄화하지 않고 데이터로 유지하는 것입니다. + + + 입력 스키마가 아직 작성되지 않은 모델은 모든 JSON 객체를 허용하는 문서화된 관대한 폴백으로 처리되므로 `422`로 응답하는 대신 본문을 전달합니다. 위 샘플은 스키마가 존재할 때 처리하는 형태를 보여줍니다. `422` 블록을 특정 본문에 대한 보장된 응답이 아닌 오류 경로로 취급하세요. + + +해당 본문에는 자체 `error_type` 필드가 없으므로 `422`에서는 `X-Comfy-Error-Type` 헤더가 머신이 읽을 수 있는 *유일한* 버킷입니다. 두 샘플 모두 바로 그 이유로 먼저 헤더에서 버킷을 읽습니다. 이는 또한 하나의 오류 클래스만으로 Router가 반환할 수 있는 모든 실패를 처리하기에 충분한 이유이기도 합니다. + +`X-Comfy-Request-Id`는 성공, `4xx`, `5xx` 할 것 없이 모든 응답에 포함되며 지원팀 요청에서 인용할 ID입니다. 두 샘플 모두 헤더 로깅을 켜고 다시 실행하여 찾도록 하는 대신 이 ID를 예외에 첨부합니다. + +## 모델 필드의 출처 + +`prompt`는 `bfl/flux-2-pro`가 요구하는 유일한 필드이며, 다음으로 자주 사용하게 될 필드는 `width`, `height`, `seed`, `output_format`입니다. 시간이 지나며 달라질 수 있는 필드 목록을 그대로 옮겨 적는 대신, 모델의 스키마를 실시간으로 확인하세요: + +```bash +curl -H "X-API-Key: $COMFY_API_KEY" \ + https://api.comfy.org/v1/models/bfl/flux-2-pro/openapi.json +``` + +이 문서는 서버가 호출을 검증할 때 사용하는 바로 그 문서로, 독립형 OpenAPI 문서로 제공됩니다. 따라서 게시된 내용과 실제로 적용되는 내용이 서로 어긋날 수 없습니다. 아무 모델 ID나 선택한 뒤 해당 호출 경로에 `/openapi.json`을 붙이면, 반환된 결과를 기준으로 생성을 진행할 수 있습니다. + +## 다음 + +- [Comfy Router API 참조](/ko/api-reference/comfy-router/reference): 모든 엔드포인트와 모든 매개변수, 그리고 15가지 오류 범주를 다룹니다. +- [Comfy Router 제한 사항](/ko/api-reference/comfy-router/limitations): 현재 Router가 지원하지 않는 기능과 대신 사용할 수 있는 방법을 설명합니다. diff --git a/ko/api-reference/comfy-router/reference.mdx b/ko/api-reference/comfy-router/reference.mdx new file mode 100644 index 000000000..609b2e40f --- /dev/null +++ b/ko/api-reference/comfy-router/reference.mdx @@ -0,0 +1,273 @@ +--- +title: "Comfy Router API 레퍼런스" +description: "Comfy API 계약에서 생성된 모든 Comfy Router 엔드포인트, 매개변수, 응답 본문 및 오류 버킷." +translationSourceHash: 1e8777df +translationFrom: api-reference/comfy-router/reference.mdx +translationBlockHashes: + "_intro": 0114a881 + "Endpoints": 6bc355f0 + "Error buckets": 04a58305 + "Response headers": 2a099bc2 + "Per-model input schemas": ae73e63b + "Schemas": a14d076e +--- + +{/* + 생성된 파일: 손으로 편집하지 마십시오. + + Comfy API 계약에서 gen_router_reference.py로 생성됩니다. 계약을 편집한 후 + 다시 생성하십시오. 여기서의 편집은 다음 실행에서 덮어써지며, 그 사이에 + 드리프트 게이트(drift gate)에서 거부됩니다. +*/} + +Comfy Router의 정식 라우트로, 모델 ID로 주소가 지정됩니다. + +기본 URL: `https://api.comfy.org` + +아래의 모든 엔드포인트에는 인증이 필요합니다. `Authorization: Bearer `를 전송하십시오. + +## Endpoints + +### `GET /v1/models` + +**List the models Comfy Router can run.** + +Comfy Router's model catalog - one page of the canonical model IDs that `POST /v1/models/{provider}/{model}` accepts. An SDK calls this on cold start to discover what is runnable, and the `model_not_found` suggestions come from the same catalog, so an ID listed here that then 404s on invocation would be worse than either failure alone. That agreement is structural rather than a promise: an entry's `provider` and `model` are the two path segments of the invocation route and reference the SAME schema components that route's path parameters do, and `id` is those two segments joined by `/`. + +**Parameters** + +| Name | In | Required | Type | Constraints | Description | +| --- | --- | --- | --- | --- | --- | +| `cursor` | query | no | [`RouterPageCursor`](#routerpagecursor) | `pattern: ^[A-Za-z0-9._~+/=-]+$`, `minLength: 1`, `maxLength: 512` | Opaque pagination cursor. Pass a previous page's `next_cursor` to fetch the next page; omit it for the first page. See `RouterPageCursor` for why the value is opaque and why this route paginates by cursor rather than by offset. | +| `limit` | query | no | integer | `maximum: 100`, `default: 20` | Number of models to return in one page. Values above the declared maximum are outside the contract, but this route does not reject them: it serves the maximum instead, and the page size actually served is echoed back as `limit` on the response, so a clamp is always detectable by the caller. Treat the maximum as the real page stride - a client that asks for more and assumes it received more will miss rows. 0 and negative values are also accepted and select the default, which is why no `minimum` is declared: sub-1 is meaningful here, not invalid. | + +**Responses** + +| Status | Body | Headers | Description | +| --- | --- | --- | --- | +| `200` | [`RouterModelListResponse`](#routermodellistresponse) | `X-Comfy-Request-Id` | OK - one page of the model catalog. | +| `400` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`. | +| `401` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`. | + +### `GET /v1/models/{provider}/{model}` + +**Read one partner model's catalog entry by canonical model ID.** + +Per-model detail for a single Comfy Router model, so a caller can check one model without walking the whole paginated catalog. The SDKs use it to look a model up immediately before invoking it. + +**Parameters** + +| Name | In | Required | Type | Constraints | Description | +| --- | --- | --- | --- | --- | --- | +| `provider` | path | yes | [`RouterProviderSegment`](#routerprovidersegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 64` | Lowercase provider segment of the canonical `{provider}/{model}[/{variant}]` model ID - the partner whose model is being run. | +| `model` | path | yes | [`RouterModelSegment`](#routermodelsegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 128` | Lowercase model segment of the canonical `{provider}/{model}[/{variant}]` model ID - the model to run within that provider. | + +**Responses** + +| Status | Body | Headers | Description | +| --- | --- | --- | --- | +| `200` | [`RouterModelDetail`](#routermodeldetail) | `X-Comfy-Request-Id` | OK - the model's catalog entry. | +| `404` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`. | + +### `POST /v1/models/{provider}/{model}` + +**Run a partner model synchronously by canonical model ID.** + +Comfy Router's canonical, model-ID-addressed entry point. The request body is the partner model's OWN native JSON input and the success response is that model's OWN native JSON output: Router forwards both unchanged instead of imposing a Comfy-shaped envelope, so a caller can move between the partner's API and Router by changing the host. This is the SYNCHRONOUS path, mirroring `POST https://fal.run/{id}` - the response carries the finished result. A queued counterpart, `/v1/queue/models/{provider}/{model}`, is planned and would put fal's `fal.run` / `queue.fal.run` split onto a single host; it is not part of this contract yet. + +**Parameters** + +| Name | In | Required | Type | Constraints | Description | +| --- | --- | --- | --- | --- | --- | +| `provider` | path | yes | [`RouterProviderSegment`](#routerprovidersegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 64` | Lowercase provider segment of the canonical `{provider}/{model}[/{variant}]` model ID - the partner whose model is being run. | +| `model` | path | yes | [`RouterModelSegment`](#routermodelsegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 128` | Lowercase model segment of the canonical `{provider}/{model}[/{variant}]` model ID - the model to run within that provider. | + +**Request body** + +`application/json` -- [`RouterModelInput`](#routermodelinput) (required) + +The partner model's native JSON input, forwarded to the provider unchanged. + +**Responses** + +| Status | Body | Headers | Description | +| --- | --- | --- | --- | +| `200` | [`RouterModelOutput`](#routermodeloutput) | `X-Comfy-Request-Id` | OK - the partner model's native JSON output, returned unchanged. | +| `403` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`. | +| `404` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`. | +| `422` | [`RouterValidationErrorResponse`](#routervalidationerrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | The request reached the model and the model rejected its contents. The body is `RouterValidationErrorResponse`, the fal/FastAPI `detail[]` shape, so each offending field keeps its own specific `type` and `ctx`. `X-Comfy-Error-Type` carries the coarse bucket for the whole response. | +| `503` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`. | +| `504` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`. | + +### `GET /v1/models/{provider}/{model}/openapi.json` + +**Read one partner model's input schema as an OpenAPI document.** + +The per-model input schema for a single Comfy Router model, served as a standalone OpenAPI document, so a caller - an SDK, a codegen tool, or an agent - can discover a model's arguments without reading Comfy's prose docs. It mirrors fal's per-model schema endpoint, and it is the discovery mechanism the SDK quickstart depends on. + +**Parameters** + +| Name | In | Required | Type | Constraints | Description | +| --- | --- | --- | --- | --- | --- | +| `provider` | path | yes | [`RouterProviderSegment`](#routerprovidersegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 64` | Lowercase provider segment of the canonical `{provider}/{model}[/{variant}]` model ID - the partner whose model is being run. | +| `model` | path | yes | [`RouterModelSegment`](#routermodelsegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 128` | Lowercase model segment of the canonical `{provider}/{model}[/{variant}]` model ID - the model to run within that provider. | + +**Responses** + +| Status | Body | Headers | Description | +| --- | --- | --- | --- | +| `200` | [`RouterModelInputSchemaDocument`](#routermodelinputschemadocument) | `X-Comfy-Request-Id`, `ETag`, `Cache-Control` | OK - the model's input schema, as a standalone OpenAPI document. | +| `304` | - | `X-Comfy-Request-Id`, `ETag`, `Cache-Control` | Not Modified - the document is unchanged since the `ETag` the caller sent in `If-None-Match`. No body is returned. | +| `404` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`. | +| `500` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | A Router request-level failure - the request never reached the model, or failed for a reason the model itself did not report. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`. | + +## Error buckets + +Coarse, machine-readable bucket for a Router failure, mirrored on the `X-Comfy-Error-Type` response header so a caller can branch without parsing the body. The set is closed at fifteen values: the six request-level buckets `invalid_input`, `content_policy_violation`, `provider_error`, `provider_timeout`, `insufficient_credits` and `model_not_found`, plus the transport-level `unauthorized`, `forbidden`, `concurrency_limit_exceeded`, `client_disconnected`, `internal_error`, `deadline_exceeded`, `not_enabled`, `service_unavailable` and `rate_limited`. + +### Request-level buckets + +Raised for a request Router accepted and then could not complete. + +| `error_type` | Meaning | +| --- | --- | +| `invalid_input` | The request was rejected before it reached the model - a malformed body, a malformed or expired pagination cursor, or an input the model's own schema does not accept. | +| `content_policy_violation` | The provider refused the request on content-policy grounds. The refusal is deterministic: re-sending the same input will be refused again. | +| `provider_error` | The partner provider reported a failure of its own, or returned a response Router could not interpret as a result. | +| `provider_timeout` | The partner provider did not answer within its deadline. This bucket is the PROVIDER timing out and never Router's own server deadline, which is reported as `deadline_exceeded` - the two share `504` and are separated because they name different causes: this one says the partner failed, that one says Comfy stopped holding the connection. | +| `insufficient_credits` | The calling workspace does not have enough credits to run the model. | +| `model_not_found` | The `{provider}/{model}` ID names no model Router can run; an unknown provider lands here too. `detail` carries up to three suggestions drawn from the models the caller is entitled to see. | + +### Transport-level buckets + +Raised by Router itself, before or around the call to the model. + +| `error_type` | Meaning | +| --- | --- | +| `unauthorized` | The request carried no usable credential. | +| `forbidden` | The credential is valid but is not entitled to this model or this operation. | +| `concurrency_limit_exceeded` | The workspace already has as many calls in flight as it is allowed; retry once one of them finishes. | +| `client_disconnected` | The caller closed the connection before Router could return a result. It is logged rather than delivered - there is no socket left to write it to - and it is an attribution, not a billing outcome: a provider generation that completed is billed regardless of whether the caller received the response. | +| `internal_error` | Router itself failed. It is also the value a client should treat any UNRECOGNIZED bucket as, so a later addition to the set does not break a client generated before it. | +| `deadline_exceeded` | Comfy stopped holding the connection at its own configured bound before an answer arrived. It shares `504` with `provider_timeout` and the pair says which side ran out of time; this one is Comfy's own bound, so nothing about the request was rejected and the same request may be retried. It says nothing about the charge: a provider generation that completed is billed regardless of whether the caller received the response. | +| `not_enabled` | Comfy Router is not switched on for this caller yet. Nothing about the request is wrong and the model exists, which is why this is not `model_not_found`; it shares `403` with `forbidden` and is NOT the same thing, because `forbidden` is an entitlement decision about the caller while this is a state of the rollout. It is TERMINAL: do not retry, and do not treat it as an outage. | +| `service_unavailable` | A service Comfy Router depends on is temporarily unavailable and the caller did nothing wrong. Retry it with backoff: it is the one bucket here whose condition clears on its own, without the caller changing the request and without a concurrency slot freeing, which is what distinguishes it from the other retryable answers (`concurrency_limit_exceeded`, `deadline_exceeded`). It is separate from `internal_error` - which is a `500` and means Router itself failed - so a client can tell "come back shortly" from "this call is not going to work". | +| `rate_limited` | The caller has spent an allowance measured over a WINDOW and must wait for that window to roll. It shares `429` with `concurrency_limit_exceeded` and is not the same thing: that one clears the moment one of the caller's own in-flight calls finishes, so retrying in seconds is right, whereas nothing the caller does drains this one early. `detail` names the window. | + +## 응답 헤더 + +| 헤더 | 유형 | 설명 | +| --- | --- | --- | +| `Cache-Control` | 문자열 | 제공되는 스키마 문서에 대한 신선도 지시문입니다. `private`은 경로가 인증되어 있기 때문입니다. 문서 자체는 호출자별로 다르지 않지만, 공유 캐시는 인증된 요청에 대한 응답을 보유해서는 안 됩니다. `must-revalidate`는 오래된 복사본이 그대로 제공되는 대신 `ETag`에 대해 재검증되도록 하기 위함입니다. | +| `ETag` | 문자열 | `GET /v1/models/{provider}/{model}/openapi.json`에 대해 제공되는 문서 바이트에 대한 강력한 엔티티 태그입니다. 모델별 스키마는 거의 변경되지 않지만 SDK가 자주 다시 가져오므로, 호출자는 이 값을 저장한 뒤 `If-None-Match`로 다시 보내 문서 대신 `304`를 받을 수 있습니다. | +| `X-Comfy-Error-Type` | [`RouterErrorType`](#routererrortype) | Router가 모든 오류 응답에 설정하는, 오류에 대한 대략적인 기계 판독 가능 버킷입니다. `RouterErrorResponse.error_type`과 동일한 값을 가지며, `422`에서는 이것이 유일한 기계 판독 가능 버킷입니다. 해당 본문이 fal/FastAPI `detail[]` 형태이고 자체 `error_type` 필드가 없기 때문입니다. 따라서 클라이언트는 수신한 두 Router 오류 본문 중 어느 것인지 결정하기 이전에 이 헤더만으로 분기할 수 있습니다. | +| `X-Comfy-Request-Id` | 문자열 | 이 호출에 대해 서버에서 생성된 식별자로, 모든 Router 응답(성공, 4xx, 5xx 모두)에 존재합니다. 오류 응답이 바로 사용자가 지원 요청에 인용할 id가 필요한 때이기 때문입니다. 동일한 값이 호출의 사용량/감사 이벤트에 기록되므로, 요금에 대한 불만을 타임스탬프로 검색하는 대신 요금 자체에 연결할 수 있습니다. | + +## 모델별 입력 스키마 + +모델의 자체 입력 필드는 여기에 다시 수록하지 않습니다. `GET /v1/models/{provider}/{model}/openapi.json`에서 실시간으로 확인하세요. 이 엔드포인트는 서버가 호출을 검증할 때 사용하는 문서와 동일한 문서를 제공하므로, 게시된 내용과 실제로 강제 적용되는 내용이 서로 어긋날 수 없습니다. `GET /v1/models`에서 모델 ID를 가져와 해당 호출 경로에 `/openapi.json`을 추가하고, 반환된 문서를 기준으로 생성을 진행하세요. + +## 스키마 + +### RouterChargesOnPolicyRejection + +이 모델이 콘텐츠 정책을 근거로 거부하는 호출이 그럼에도 불구하고 호출자에게 청구되는지 여부를 나타냅니다. 공급자마다 다르며, 그 차이는 호출 시점에 드러나지 않습니다. 동일한 호출에 대해 오류와 청구를 함께 확인한 사용자는 이를 알 도리가 없습니다. 따라서 이는 공급자별 구전 지식에 맡겨지지 않고, 호출 이전에 모델별로 명시됩니다. + +타입: `string`### RouterErrorResponse + +Router의 요청 수준 오류 본문: 요청이 모델에 도달하지 못했거나, 모델 자체가 신고하지 않은 이유(인증, 할당량, 알 수 없는 모델 ID, 공급자 전송)로 실패한 경우 반환되는 내용입니다. 모델 수준 검증 실패는 `RouterValidationErrorResponse`라는 자체 형태를 가집니다. FastAPI의 `detail[]` 배열을 이 `detail` 문자열로 평탄화하면 SDK가 분기하는 필드별 세분성이 손상되기 때문입니다. + +| 필드 | 유형 | 필수 | 제약 조건 | 설명 | +| --- | --- | --- | --- | --- | +| `detail` | string | 예 | - | 실패에 대한 사람이 읽을 수 있는 설명으로, 최종 사용자에게 표시해도 안전합니다. 기계가 파싱하지 않으므로 대신 `error_type`으로 분기하세요. | +| `error_type` | [`RouterErrorType`](#routererrortype) | 예 | - | Router 실패에 대한 대략적이고 기계가 읽을 수 있는 분류로, `X-Comfy-Error-Type` 응답 헤더에도 동일하게 반영되므로 호출자가 본문을 파싱하지 않고 분기할 수 있습니다. 값 집합은 15개로 고정되어 있습니다. 여섯 가지 요청 수준 분류인 `invalid_input`, `content_policy_violation`, `provider_error`, `provider_timeout`, `insufficient_credits`, `model_not_found`와 전송 수준 분류인 `unauthorized`, `forbidden`, `concurrency_limit_exceeded`, `client_disconnected`, `internal_error`, `deadline_exceeded`, `not_enabled`, `service_unavailable`, `rate_limited`가 있습니다. |### RouterErrorType + +Router 오류를 대략적이고 기계가 읽을 수 있는 버킷으로 분류한 것으로, `X-Comfy-Error-Type` 응답 헤더에도 반영되므로 호출자가 본문을 파싱하지 않고도 분기할 수 있습니다. 이 집합은 15개의 값으로 고정되어 있습니다: 요청 수준의 6개 버킷인 `invalid_input`, `content_policy_violation`, `provider_error`, `provider_timeout`, `insufficient_credits`, `model_not_found`와 전송 수준의 `unauthorized`, `forbidden`, `concurrency_limit_exceeded`, `client_disconnected`, `internal_error`, `deadline_exceeded`, `not_enabled`, `service_unavailable`, `rate_limited`입니다. + +유형: `string`### RouterModelBilling + +모델별 청구에 관한 사실은 호출자가 호출 이전에 알아야 할 내용으로, 가격이 아닙니다. 사용량 및 비용 수치는 여기에 표시되지 않습니다. + +| 필드 | 유형 | 필수 | 제약 조건 | 설명 | +| --- | --- | --- | --- | --- | +| `charges_on_policy_rejection` | [`RouterChargesOnPolicyRejection`](#routerchargesonpolicyrejection) | 예 | - | 이 모델이 콘텐츠 정책에 따라 거부한 호출에 대해서도 호출자에게 청구되는지 여부입니다. 공급자마다 다르며, 그 차이는 호출 시간에 확인할 수 없습니다. 같은 호출에 대해 오류와 청구를 모두 확인한 사용자는 이를 알 방법이 없으므로, 공급자별 관례에 맡기는 대신 호출 이전에 모델별로 명시됩니다. |### RouterModelDetail + +Comfy Router 모델 하나에 대한 모델별 세부 정보: 카탈로그 목록이 해당 모델에 대해 보고하는 모든 정보와, 단일 모델 라우트에서만 제공되는 모델별 필드를 포함합니다. + +[`RouterModelListEntry`](#routermodellistentry)와 [`RouterModelDetailFields`](#routermodeldetailfields)로 구성됩니다. + +유형: `object`### RouterModelDetailFields + +`RouterModelDetail` 중 카탈로그 목록이 담지 않는 절반: 한 번의 조회로 충분하지만 페이지네이션된 카탈로그 페이지의 모든 항목에 반복할 가치가 없는 모델별 필드입니다. + +| 필드 | 타입 | 필수 | 제약 조건 | 설명 | +| --- | --- | --- | --- | --- | +| `input_schema_url` | 문자열 | 아니요 | `format: uri`, `pattern: ^https://`, `maxLength: 2048` | 이 모델의 입력 스키마 문서를 가리키는 포인터입니다. 입력 스키마 문서는 이 모델에 대해 `POST /v1/models/{provider}/{model}`이 받아들이는 본문(body)의 설명입니다. 오직 포인터만이 이 계약의 일부입니다. 포인터가 가리키는 문서는 별도로 작성됩니다. 모델에 대한 스키마가 작성되지 않은 경우에는 이 필드가 존재하지 않습니다. |### RouterModelId + +`{provider}/{model}` 형식의 표준 Comfy Router 모델 ID입니다. 이 값은 `POST /v1/models/{provider}/{model}`에서 모델을 주소 지정하는 값과 정확히 일치하므로, 호출자는 다른 곳에서 다시 파생할 필요 없이 해당 경로에 바로 삽입할 수 있습니다. `pattern`은 단일 `/`로 연결된 `RouterProviderSegment`와 `RouterModelSegment`이며, `maxLength`는 두 값의 합에 해당 구분자를 더한 값입니다. + +유형: `string`. `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*/[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 193`### RouterModelInput + +파트너 모델의 네이티브 JSON 입력 문서로, 공급자에게 있는 그대로 전달됩니다. 구체적인 형태는 Comfy가 아닌 파트너가 소유하므로 이는 개방형 객체입니다. Router는 필드의 범위를 좁히거나 이름을 바꾸거나 다시 감싸지 않습니다. ComfyUI의 스펙 기반 코드 생성이 생성할 클래스를 필요로 하기 때문에 명명된 컴포넌트입니다(인라인 익명 객체는 절대 아님). + +유형: `object`### RouterModelInputSchemaDocument + +단일 Comfy Router 모델의 입력을 설명하는 독립 OpenAPI 문서로, 해당 모델에 대해 `POST /v1/models/{provider}/{model}`가 허용하는 요청 본문입니다. `GET /v1/models/{provider}/{model}/openapi.json`이 반환하는 내용이기도 합니다. + +유형: `object`### RouterModelListEntry + +Router 모델 카탈로그의 한 항목입니다. 실행 가능한 모델의 식별 정보만을 담으며, 그 외의 다른 것은 포함하지 않습니다. 모델별 상세 라우트는 이 동일한 항목을 반복해서 기술하는 대신 이 항목을 조합하여 사용합니다. 따라서 이름이 `...Summary`가 아니라 `...ListEntry`인 이유는, 카탈로그 항목이 무엇인지에 대한 정의가 정확히 하나만 존재해야 하기 때문입니다. 모델별 상세 및 모델별 입력/출력 스키마는 각각 별도의 라우트이므로, 이 형태는 호출자가 모델을 호출하는 데 필요한 최소한의 정보로 유지됩니다. 이는 의도적인 설계입니다. SDK가 콜드 스타트 시 가져오는 페이로드가 바로 이것이기 때문입니다. `id`는 `provider`와 `model`을 `/`로 연결한 값입니다. 두 필드는 별도로도 제공되므로 호출자는 문자열을 분할하지 않고 호출 경로를 구성할 수 있습니다. + +| 필드 | 타입 | 필수 | 제약 조건 | 설명 | +| --- | --- | --- | --- | --- | +| `id` | [`RouterModelId`](#routermodelid) | 예 | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*/[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 193` | 정규 Comfy Router 모델 ID인 `{provider}/{model}`입니다. `POST /v1/models/{provider}/{model}`에서 모델을 주소 지정하는 값과 정확히 일치하므로, 호출자는 다른 어떤 값에서도 다시 파생할 필요 없이 해당 경로에 그대로 삽입할 수 있습니다. 이 `pattern`은 `RouterProviderSegment`와 `RouterModelSegment`를 단일 `/`로 연결한 것이며, `maxLength`는 두 값의 합에 해당 구분자를 더한 값입니다. | +| `provider` | [`RouterProviderSegment`](#routerprovidersegment) | 예 | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 64` | 정규 `{provider}/{model}[/{variant}]` 모델 ID의 소문자 `provider` 세그먼트입니다. 모델이 주소 지정되는 파트너를 나타냅니다. 호출 라우트의 `provider` 경로 매개변수와 카탈로그 항목의 `provider` 필드는 모두 이 하나의 스키마를 참조하므로, 목록의 ID와 허용되는 ID가 서로 어긋나지 않습니다. | +| `model` | [`RouterModelSegment`](#routermodelsegment) | 예 | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 128` | 정규 `{provider}/{model}[/{variant}]` 모델 ID의 소문자 `model` 세그먼트입니다. 해당 공급자 내에서 실행할 모델을 나타냅니다. 호출 라우트의 `model` 경로 매개변수와 카탈로그 항목의 `model` 필드는 `RouterProviderSegment`와 동일한 불일치 방지 이유로 이 스키마를 공유합니다. | +| `billing` | [`RouterModelBilling`](#routermodelbilling) | 예 | - | 호출자가 호출 이전에 알아야 하는 모델별 청구 정보입니다. 가격이 아닙니다. 사용량 및 비용 수치는 여기에 절대 나타나지 않습니다. |### RouterModelListResponse + +Router 모델 카탈로그의 한 페이지입니다. + +| 필드 | 유형 | 필수 | 제약 조건 | 설명 | +| --- | --- | --- | --- | --- | +| `data` | [`RouterModelListEntry`](#routermodellistentry)의 배열 | 예 | - | 이 페이지에 포함된 모델로, 최대 `limit`개입니다. | +| `has_more` | 논리값 | 예 | - | 이 페이지 이후에 다른 페이지가 더 존재하는지 여부입니다. 이 값이 참인 동안 계속 탐색하십시오. `data`가 짧거나 비어 있더라도 카탈로그의 끝으로 유추하지 마십시오. | +| `next_cursor` | [`RouterPageCursor`](#routerpagecursor) | 아니요 | `pattern: ^[A-Za-z0-9._~+/=-]+$`, `minLength: 1`, `maxLength: 512` | Router 목록에 대한 불투명(opaque) 커서입니다. 서버가 생성하며 클라이언트에 전달되었다가 그대로 반환될 뿐입니다. 오프셋도 아니고, 모델 ID도 아니며, 정렬되지 않고, 카탈로그가 다시 구축될 때에도 안정적이지 않습니다. 따라서 이 커서를 구문 분석하거나, 증가시키거나, 커서가 나온 탐색 범위를 넘어 보존하는 것은 모두 계약 범위 밖입니다. 오프셋 대신 커서를 사용하는 이유는 카탈로그가 계속 변하는 목록이기 때문입니다. 탐색 중간에 항목이 추가되거나 제거되면 오프셋 기반 탐색은 항목을 조용히 건너뛰거나 반복하게 되며, 호출자는 그 사실을 알 수 없습니다. | +| `limit` | 정수 | 예 | `minimum: 1`, `maximum: 100` | 실제로 제공된 페이지 크기입니다. 최대값을 초과하는 `limit` 요청은 거부되지 않고 최대값으로 제한(clamp)됩니다. 따라서 이 값은 요청한 값보다 작을 수 있습니다. 페이지네이션에는 보낸 값이 아닌 이 값을 사용하십시오. 그렇지 않으면 실제로 수신하지 못한 행이 있다고 가정하게 됩니다. |### RouterModelOutput + +파트너 모델의 네이티브 JSON 출력 문서로, 호출자에게 있는 그대로 반환됩니다. 구체적인 형태는 Comfy가 아닌 파트너가 소유하므로, 이는 개방형 객체(open object)입니다. Router는 필드를 좁히거나, 이름을 바꾸거나, 다시 래핑하지 않습니다. ComfyUI의 스펙 기반 코드 생성(codegen)에는 생성할 클래스가 필요하기 때문에, 이는 named 컴포넌트입니다(인라인 익명 객체가 아닙니다). + +유형: `object`### RouterModelSegment + +표준 `{provider}/{model}[/{variant}]` 모델 ID의 소문자 `model` 세그먼트: 해당 공급자 내에서 실행할 모델입니다. `RouterProviderSegment`와 동일한 드리프트 방지 이유로 호출 라우트의 `model` 경로 파라미터와 카탈로그 항목의 `model` 필드에서 공유됩니다. + +타입: `string`. `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 128`### RouterPageCursor + +Router 목록에 대한 OPAQUE 커서입니다. 서버에 의해 생성되며 항상 왕복 전송만 됩니다. 즉, 오프셋도, 모델 ID도, 정렬된 값도 아니며, 카탈로그 재구축 시에도 안정적이지 않습니다. 따라서 커서를 파싱하거나 증가시키거나, 커서가 생성된 탐색(walk) 범위를 벗어나 보관하는 것은 모두 계약 범위 밖입니다. 오프셋 대신 커서를 사용하는 이유는 카탈로그가 계속 변하는 목록이기 때문입니다. 오프셋 탐색은 탐색 중 항목이 추가되거나 제거되면 항목을 소리 없이 건너뛰거나 반복하게 되며, 호출자는 그런 일이 발생했는지 알 수 없습니다. + +Type: `string` -- `pattern: ^[A-Za-z0-9._~+/=-]+$`, `minLength: 1`, `maxLength: 512`### RouterProviderSegment + +정규 `{provider}/{model}[/{variant}]` 모델 ID에서 소문자 `provider` 세그먼트로, 요청 대상 모델의 파트너를 나타냅니다. 호출 라우트의 `provider` 경로 매개변수와 카탈로그 항목의 `provider` 필드는 모두 이 하나의 스키마를 참조하므로, 나열된 ID와 허용되는 ID가 서로 어긋나지 않게 유지됩니다. + +유형: `string` - `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 64`### RouterValidationErrorContext + +하나의 `RouterValidationErrorDetail`에 대해 위반된 한도이며, 공급자로부터 그대로 전달됩니다. 예를 들어 `greater_than`과 함께 `{"limit_value": 8}`, `image_too_small`과 함께 `{"min_width": 512}`, 또는 `file_too_large`와 함께 `{"max_size_bytes": 10485760}`이 이에 해당합니다. 키 집합은 공급자와 오류 유형에 따라 달라지므로, 이 객체는 의도적으로 열린 객체입니다. 이를 고정된 필드 목록으로 좁히거나 `msg` 문자열로 접으면, 포팅된 통합이 컴파일은 되지만 한도를 읽던 분기를 조용히 잃어버리는 결과를 낳습니다. 오류 유형에 한도가 없는 경우에는 이 값이 없습니다. + +유형: `object`### RouterValidationErrorDetail + +fal/FastAPI 형식의 모델 수준 검증 오류 하나입니다. `type`은 특정 공급자 사유(`value_error`, `missing`, `image_too_small`, `unsupported_audio_format`, `greater_than`, `file_too_large` 등)를 담으며, 이는 `RouterErrorType`의 대략적인 분류가 표현할 수 없는 세부 수준입니다. 같은 이유로 이 값은 `enum`이 아닌 개방형 문자열(open string)입니다. 공급자 어휘는 두 계층에 걸쳐 약 48개 값에 달하며, 우리가 아니라 공급자의 릴리스 주기에 따라 늘어납니다. 따라서 모델링되지 않은 값은 역직렬화에 실패하기보다 호출자에게 도달해야 합니다. + +| 필드 | 유형 | 필수 | 제약 조건 | 설명 | +| --- | --- | --- | --- | --- | +| `loc` | 모든 유형의 배열 | 예 | - | 문제가 되는 필드의 경로로, 가장 바깥쪽 세그먼트가 먼저 옵니다. 예를 들어 `["body", "image_url"]` 또는 `["body", "images", 0]`이며, 여기서 정수는 배열의 인덱스를 나타냅니다. | +| `msg` | 문자열 | 예 | - | 이 단일 실패에 대한 사람이 읽을 수 있는 설명입니다. | +| `type` | 문자열 | 예 | - | 이 실패에 대한 구체적이고 기계가 읽을 수 있는 사유로, 공급자로부터 변경 없이 그대로 전달됩니다. 타입이 지정된 SDK 예외 계층 구조가 분기하는 기준이 되는 값이며, 응답 헤더의 `error_type`은 그 대략적인 분류일 뿐입니다. | +| `ctx` | [`RouterValidationErrorContext`](#routervalidationerrorcontext) | 아니요 | - | 하나의 `RouterValidationErrorDetail`에 대해 위반된 제약 조건으로, 공급자로부터 그대로 전달됩니다. 예를 들어 `greater_than`과 함께 `{"limit_value": 8}`, `image_too_small`과 함께 `{"min_width": 512}`, 또는 `file_too_large`와 함께 `{"max_size_bytes": 10485760}`이 있습니다. 키 집합은 공급자와 오류 유형에 따라 다르므로 의도적으로 개방형 객체(open object)로 남겨 둡니다. 이를 고정된 필드 목록으로 좁히거나 `msg` 문자열에 통합하면, 이식된 통합이 컴파일은 성공하지만 제약 조건을 읽는 분기를 조용히 잃어버리게 됩니다. 오류 유형에 제약 조건이 없는 경우에는 이 필드가 없습니다. | +| `input` | [`RouterValidationErrorInput`](#routervalidationerrorinput) | 아니요 | - | 문제가 되는 입력 값으로, 호출자가 `loc`에서 다시 도출하지 않고 무엇이 거부되었는지 확인할 수 있도록 있는 그대로 다시 전달됩니다. 모든 JSON 유형(문자열, 숫자, 논리값, 배열, 객체 또는 null)이 가능하므로 이 스키마는 객체로 좁히지 않고 의도적으로 유형을 지정하지 않은 상태로 둡니다. 공급자가 입력을 다시 전달하지 않는 경우에는 이 필드가 없습니다. |### RouterValidationErrorInput + +문제가 되는 입력 값으로, 호출자가 `loc`에서 다시 도출하지 않고도 거부된 값이 무엇인지 확인할 수 있도록 있는 그대로 반환됩니다. 문자열, 숫자, 논리값, 배열, 객체 또는 null 등 모든 JSON 유형이 될 수 있으므로, 이 스키마는 객체로 한정하지 않고 의도적으로 유형을 지정하지 않은 채로 둡니다. 공급자가 입력을 다시 반환하지 않는 경우에는 이 필드가 존재하지 않습니다.### RouterValidationErrorResponse + +Router의 모델 수준 `422` 본문으로, fal/FastAPI 형식입니다. 요청이 모델에 도달할 수 있을 만큼 형식이 올바르게 갖추어졌지만, 모델이 그 내용을 거부했음을 의미합니다. 자체적으로 `error_type`을 포함하지 않는다는 점에 유의하세요. 그 역할은 응답의 `X-Comfy-Error-Type`이 담당하므로, 클라이언트는 수신한 두 가지 Router 오류 본문 중 어떤 것인지 먼저 판단하지 않고도 헤더에서 대략적인 분류를 읽을 수 있습니다. + +| 필드 | 유형 | 필수 | 제약 조건 | 설명 | +| --- | --- | --- | --- | --- | +| `detail` | [`RouterValidationErrorDetail`](#routervalidationerrordetail)의 배열 | 예 | - | 요청에서 발견된 모든 검증 실패로, 문제가 있는 필드당 하나의 항목입니다. | diff --git a/ko/api-reference/v2/overview.mdx b/ko/api-reference/v2/overview.mdx index 42baab437..d9bf2d888 100644 --- a/ko/api-reference/v2/overview.mdx +++ b/ko/api-reference/v2/overview.mdx @@ -1,7 +1,7 @@ --- title: "Comfy API v2 개요" description: "공식 Comfy API v2 레퍼런스: 입력을 업로드하고 작업을 제출하며 결과를 폴링하여 외부 애플리케이션에서 ComfyUI 워크플로우를 실행합니다." -translationSourceHash: c3d19ef0 +translationSourceHash: 0a5bed89 translationFrom: api-reference/v2/overview.mdx --- @@ -35,3 +35,7 @@ translationFrom: api-reference/v2/overview.mdx |----------|-------------| | 에셋 | 콘텐츠 주소 지정 blob을 기반으로 하는 UUID 식별 레코드입니다. 입력을 업로드하고 출력을 다운로드합니다. | | 작업 | 워크플로의 단일 실행입니다. 영속적이며, 폴링 가능하고, 취소 가능합니다. | + +## Comfy Router + +Comfy API v2는 제출 후 폴링하는 영속적인 작업(job)으로 워크플로를 실행합니다. 모델을 직접 호출해야 하는 경우(파트너 모델 하나, 요청 하나, 모델 고유의 네이티브 입력 및 출력)에는 [Comfy Router](/ko/api-reference/comfy-router/quickstart)를 참조하세요. diff --git a/openapi-v2.yaml b/openapi-v2.yaml index 7d3801366..d1a51be86 100644 --- a/openapi-v2.yaml +++ b/openapi-v2.yaml @@ -556,7 +556,7 @@ paths: description: 'Emitted the moment each output asset is committed, carrying the same `Output` object that appears on `job.outputs[]`. A latency optimization only: it lets a client render each result as it lands instead of waiting for the terminal `status` event. It is delivered best-effort over the live broadcast path — an output whose durable asset record is not yet resolvable when its node finishes may be delivered on a slightly later event or, failing that, only in the terminal `status` snapshot — so the authoritative, complete set of outputs is always `job.outputs[]` on `GET /api/v2/jobs/{id}` and on the terminal `status` event. A client must therefore treat these as additive hints and must not assume it receives one per output.' schema: '#/components/schemas/Output' log: - description: Selected execution log lines. Best-effort diagnostics; the one event type with no snapshot equivalent. NOT YET EMITTED by the server in the first iteration — reserved in the catalog so the wire contract is stable. Clients must not depend on receiving this event yet. + description: 'Selected execution log lines. Best-effort diagnostics. Its snapshot equivalent is `job.logs` on `GET /api/v2/jobs/{id}`, which carries the whole log the run produced, read back once the run has finished; this event is the live view of that same output, carrying lines while the run is still going. NOT YET EMITTED by the server in the first iteration — reserved in the catalog so the wire contract is stable. Clients must not depend on receiving this event yet: to get a log today, stream to a terminal status and re-read the job.' x-sse-not-yet-emitted: true schema: '#/components/schemas/LogEvent' parameters: @@ -810,6 +810,10 @@ components: allOf: - $ref: '#/components/schemas/JobError' nullable: true + logs: + allOf: + - $ref: '#/components/schemas/JobLogs' + description: 'What the run printed. **Only jobs run on the serverless platform** (a `{deployment}.run.comfy.app` host) carry it. Comfy Cloud and self-hosted callers never receive it, so on those surfaces the field is always absent and a client should not wait for one. Where it is populated it is captured for every job, success and failure alike, since a job that succeeds while producing the wrong thing is exactly what a failure-only log cannot explain. It lives as long as the job it belongs to: nothing ages it out ahead of the job''s own `expires_at`, so a job never outlives its log. **Absent, not null**, when there is none: the surface does not populate it at all, the job has not finished, the job predates log capture, or the job ran on the public demo deployment, which captures and stores the log like every other serverless deployment but withholds it on read, because that surface takes callers with no credential and a job id would otherwise be the only thing between one anonymous caller and another''s run. Those cases are deliberately not distinguished, because a caller''s next action is the same in all of them, which is to stop expecting a log. Returned by `GET /api/v2/jobs/{id}` only. It is deliberately absent from the job object on `POST /api/v2/jobs`, on `POST /api/v2/jobs/{id}/cancel`, and on the SSE `status` event: the last is pushed on every transition to every open stream, and a log on each frame would pay for the whole thing repeatedly to deliver it once. A client that streams to a terminal status and wants the log re-reads the job.' metrics: type: object description: 'Values are nullable (a metric not yet available — e.g. `execution_ms` before a job starts running — is `null`, not omitted); the example below is deliberately all-non-null purely to work around a Spectral/nimma lint-tooling crash on a literal `null` inside a schema `example` combined with `additionalProperties.nullable: true` — the schema itself is unchanged and still allows null values at runtime.' @@ -821,6 +825,24 @@ components: execution_ms: 42000 urls: $ref: '#/components/schemas/JobUrls' + JobLogs: + type: object + description: 'A job''s captured execution log. Diagnostics, not a contract on content: this is whatever the workflow''s own code and nodes wrote to standard output, in the order they wrote it, so nothing about its shape is stable between runs or between releases of a build. It is **untrusted text** — a workflow chooses what goes in it — and must be rendered as plain text rather than interpreted.' + required: + - text + - truncated + - captured_at + properties: + text: + type: string + description: The captured output. + truncated: + type: boolean + description: '`text` is the TAIL of a longer run. Implementations bound what they capture and store, so a workflow that prints megabytes keeps its last lines — where a failure normally is — instead of being dropped whole. True with an empty `text` means the log was captured and then shed entirely to fit.' + captured_at: + type: string + format: date-time + description: When the run's output was read back off the worker. JobWorkflowResponse: type: object description: The workflow behind a job. See GET /api/v2/jobs/{id}/workflow's description for exactly when `format` is `save` vs `api`. diff --git a/zh/api-reference/comfy-router/limitations.mdx b/zh/api-reference/comfy-router/limitations.mdx new file mode 100644 index 000000000..3f7ba5860 --- /dev/null +++ b/zh/api-reference/comfy-router/limitations.mdx @@ -0,0 +1,114 @@ +--- +title: "Comfy Router 的局限性" +description: "Comfy Router 目前无法做到的事、存在替代方案时该改用何种方案,以及其中哪些限制预计将会改变。" +translationSourceHash: d49d86b7 +translationFrom: api-reference/comfy-router/limitations.mdx +translationBlockHashes: + "_intro": c72d3766 + "At a glance": dd3cde31 + "No queued submission": f24e2b9d + "No cost or credit figures on a response": 0d8505a9 + "No way to resume a call you lost": 99da3819 + "Calls are cut off at a server deadline": 29b27f1c + "No progress while a call runs": 40667680 + "Three forecast buckets are not in the vocabulary": 6f256b4e + "Router does not cover every partner operation": 069b148a + "Next": c839d9e8 +--- + +**Comfy Router 尚未全面可用。** 下文引用的路由: +`POST /v1/models/{provider}/{model}` 及其目录和架构相关端点,目前尚未提供服务请求:经过身份验证的调用现在会返回 `404`。本页描述的是它们将要提供的契约,并在该上线之前发布,以便集成可以针对已知形状进行编写。以下所有内容都是关于该契约的说明,而不是你现在可以实践的行为。 + + +Comfy Router 是一次同步调用:你使用一个凭据,将合作伙伴模型的原生输入发送到一个主机,连接保持打开,`200` 响应携带该模型的原生输出。这种形状正是让首次集成变得简短的原因,也是本页所有限制的来源。在围绕 Router 进行设计之前阅读本页,而不是之后:下文大部分内容都有直接的替代方案,而那些没有替代方案的内容,值得你在基于 Router 并不成立的假设进行构建之前了解。 + +## 概览 + +每一行都会链接到详细说明该限制的章节。**设计使然**表示该限制是 Router 工作方式的一部分,不依赖任何待实现的功能;**尚未支持**表示 Router 预计将获得该能力,但本页面不对具体时间做出任何承诺。 + +| 限制 | 替代方案 | 状态 | +| --- | --- | --- | +| [无排队提交:调用为同步操作](#不支持排队提交) | 保持连接打开,或使用合作伙伴代理路由进行提交和轮询 | 尚未支持 | +| [响应中不包含费用或额度数据](#响应中不包含费用或积分数字) | 在 Comfy 平台查看余额和使用量;调用之前查看模型目录条目上的 `billing` 字段 | 尚未支持 | +| [无法恢复已丢失的调用](#无法恢复已丢失的调用) | 发送 `Idempotency-Key`,这样重试最多只会计费一次,但无法恢复已丢失的结果 | 尚未支持 | +| [调用会在服务器截止时间被切断](#调用会在服务器截止时间被切断) | 为客户端设置高于截止时间的超时;拆分无法在截止时间内完成的工作 | 设计使然 | +| [调用运行期间不提供进度信息](#调用运行期间无进度) | Router 目前没有此类功能;合作伙伴代理路由可能会提供自身的进度信息 | 尚未支持 | +| [三种预测分桶不在词汇表中](#三个预告的分类不在词汇表中) | 处理 Router 提供的十五种分桶;将任何无法识别的值视为 `internal_error` | 尚未支持 | +| [Router 不覆盖所有合作伙伴操作](#router-并不覆盖每个合作伙伴操作) | 使用同一主机上 `/proxy/…` 下的合作伙伴代理路由 | 设计使然 | + +## 不支持排队提交 + +运行模型只有一种方式:`POST /v1/models/{provider}/{model}`,该端点会保持连接,直到生成完成并在响应中返回结果。没有接受任务后返回标识符、让你稍后再来获取结果的端点,也没有完成时的回调或 webhook。计划中有一个对应的排队端点,在 API 参考中写作 `/v1/queue/models/{provider}/{model}`;它目前不属于契约的一部分,对它发起调用不会被处理。 + +**替代做法。** 对大多数模型来说这不成问题:保持连接打开并读取结果即可。快速的图像模型几秒钟即可返回;长时间的视频生成可能运行数分钟,Router 会为其一直保持连接。设置一个宽裕的客户端读取超时,要高于 [Router 自身的截止时间](#调用会在服务器截止时间被切断),并将该调用视为长时间运行的请求,而非快速请求。如果你的架构确实无法保持连接打开,例如执行上限很短的 serverless 函数,或你预期用户会关闭的浏览器标签页,那么就从你能控制且能够保持连接的 worker 发起调用,或者使用合作伙伴代理路由,选择提供自身提交和轮询机制的提供商。参见[最后一节](#router-并不覆盖每个合作伙伴操作)。 + +**状态:尚未提供。** 排队路径属于预期功能;本页不承诺具体时间。 + +## 响应中不包含费用或积分数字 + +Router 响应会告诉你模型生成了什么,但它的契约不涉及任何成本信息。响应体中不包含收费金额、积分余额或使用量数字,路由也不声明任何成本标头。有一个注意事项,以免你感到意外:Router 与合作伙伴代理路由共享同一条计费路径,该路径在允许列表中的提供商的计费响应上标记 `X-Comfy-Credits-Used`,因此当你调用其中某个提供商时,该标头可能会出现在 Router 响应中。它不属于 Router 契约的一部分:对于允许列表之外的每个提供商,该标头都不会出现,并且它特意*不会*在幂等重试时重放,其目的正是为了防止汇总该标头的客户端对仅支付过一次的调用进行重复计数。不要基于它来对账。模型目录也是如此:它包含调用方在调用之前所需的计费*事实*,而从不包含价格。因此,你无法仅凭 Router 响应来核对支出,也无法在未从其他地方获得 X 的情况下向用户显示“这次调用花了 X”。 + +**替代做法。** 你的余额、使用量和账单都保存在 Comfy 平台的 [platform.comfy.org](https://platform.comfy.org):这是你已支出和剩余金额的事实来源,且不受本页任何内容的影响。Router 在调用时确实会告诉你两件值得利用的事情:因积分不足而被拒绝的调用会返回 `insufficient_credits`,这样你就可以将积分耗尽作为类型化错误来处理,而无需预先检查余额;另外,每个模型的目录条目都带有 `billing.charges_on_policy_rejection`,它表明该特定模型是否会基于内容政策拒绝生成并因此向你收费。它是一个**包含三个值的字符串**,而不是布尔值:`yes`、`no` 和 `unknown`。请将 `unknown` 理解为“这可能会向你收费”:它意味着还没有人确定该模型的行为,而它存在正是为了使未经检查的模型不会以 `no` 形式发布,因为 `no` 是一种断言。该字段刻意不是 `enum`,因此请将任何你无法识别的值也视为 `unknown`,并且不要对它进行真值检查:字符串 `"no"` 在大多数语言中是真值,这样的检查会让这个字段本来要捕获的情况反转。不同提供商在这方面存在差异,这种差异在调用时不可见,而在调用之前读取它,正是为了避免事后出现无法解释的收费。 + +**状态:尚未提供**逐次调用的数字。请注意,*目录*刻意不携带价格:定价属于维护价格的地方,而不会重复复制到可能与之偏离的模型列表中。 + +## 无法恢复已丢失的调用 + +Router 不会保留进行中调用的可恢复记录。没有状态路由,没有任务标识符,也没有任何可重新连接的对象。如果连接在调用中途断开(客户端崩溃、网络分区、重启进程的部署),响应便不复存在,之后你也无从查询这次调用。*生成*是否已完成并被计费,与你是否收到它,是两个不同的问题,而丢失连接并不能可靠地回答其中任何一个。 + +**应该怎么做。** 在每次调用中发送 `Idempotency-Key` 请求头。它不能让已丢失的调用恢复,但能让重试变得安全。Router 会在调用时长内保留该密钥;当调用确实将答案送达你时,Router 会将该响应记在该密钥下并保留 24 小时。使用**相同**密钥重试时,便会重放已记录的响应,而不是第二次向提供商分派请求(并再次计费),并标记为 `Idempotent-Replayed: true`,以便你区分重放与全新运行。请为每个逻辑调用生成一个新密钥,而不是每次尝试都生成新密钥。以*不同*请求体出示相同密钥会返回 `409`,而不是静默覆盖。 + +请准确理解这能给你带来什么,因为这是**计费**属性,而不是投递属性:**一个密钥最多只计费一次。** 它并不是承诺一个密钥最多只向提供商分派一次。Router 会为你实际收到的答案保留密钥;任何未向你收费的结果都会释放密钥,使调用可以再次进行。`5xx`、`408`/`425`/`429`,以及(这里最关键的一种情况)完全没有内容到达你的调用:这些情况都会释放密钥,使用该密钥重试会真正重新执行,并重新分派给提供商。 + +**因此,连接断开正是幂等性*无法*挽救的情况。** 调用中途丢失连接通常意味着从未有任何响应真正交付给你,这正是上述的释放路径:使用相同密钥重试会开启全新运行,而不是把你错过的结果交给你。如果原始生成已经被分派,提供商可能会第二次运行它。这是正确的默认行为:你从未收到且未被计费的调用应当可以重新运行。但请按“重试会产生新运行”来规划,而不是“重试会找回丢失的运行”。 + +当 Router *确实*为密钥保留了内容时,重试会得到应答而不是重新执行:要么重放原始响应,要么返回 `409` 说明无法重放的原因。在原始调用仍在进行中时发送的重试会返回携带 `Retry-After` 的 `409`,因此请等待后再重新发送相同的密钥。针对已完成但其响应 Router 无法保留忠实副本的调用进行重试,也会返回 `409`。这不仅限于响应过大的情况:超过重放上限的响应、应答后失败或崩溃的处理器,以及向你写入时失败或写入不足,都会将密钥记录为已消费但不可重放,并返回相同的 `409`。看到这个错误时,不要去找大小问题。上述所有情况下的引导都是一样的:使用**新**密钥。原始调用已完成并被计费,Router 既不会凭空捏造其响应,也不会在旧密钥下重新运行它。 + + +**已生成的合同中尚无这些内容。** 此处描述的 `Idempotency-Key` 请求头、`409` 响应以及 `Idempotent-Replayed` 和 `Retry-After` 响应头,并未在生成参考文档所依据的 OpenAPI 合同中的 `POST /v1/models/{provider}/{model}` 上声明,因此它们不会出现在生成的 API 参考中,SDK 也不会对它们进行建模。在获得支持之前,请自行发送和读取这些内容。 + + +**状态:尚未支持。** 持久化、可恢复的执行预计将随队列式路径一同推出,届时请求记录将有处可存。幂等重试目前就是答案,而且它不是临时方案:无论如何都值得集成。 + +## 调用会在服务器截止时间被切断 + +一次 Router 调用可将连接保持 **10 分钟**。这是默认值;它是服务器端配置值,而不是固定常量,因此应将其视为设计时依据的数字,而不是合同里写死的保证。超过该时间后,Router 不再等待,会取消自身发送给提供商的进行中请求,并以 `X-Comfy-Error-Type: deadline_exceeded` 返回 `504`。**`deadline_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 参考](/zh/api-reference/comfy-router/reference) 中列出且快速入门所指的那十五个。该参考文档的正文中还另行提及三个作为预期新增项:`file_download_error`、`cancelled` 和 `queue_timeout`。它们只是被提及,仅此而已。它们目前**不是该集合的成员**:没有任何 Router 响应携带它们,根据契约生成的客户端也不认识它们,而且如果 Router 在内部收到其中一个,它会用 `internal_error` 代替,而不是将其作为响应发出。因此,你今天为它们编写的分支是永远不会运行的分支,而且它们在参考文档中的出现并不能证明 Router 会取消调用或将它们排队:它两者都不会做。 + +它们以书面形式预告出来,而不是被完全省略,因为 `error_type` 刻意设计为普通字符串而非 `enum`,而一个硬性拒绝无法识别分类的客户端,恰恰会在事情已经出错的时候失败得最严重。提前说出这些新增项,正是为了让读者知道该集合在设计上就是开放式的。 + +**应该怎么做。** 处理 Router 实际发布的十五个分类,完整列表见 [API 参考](/zh/api-reference/comfy-router/reference),并编写一个将任何无法识别的值视为 `internal_error` 的回退分支。这个回退分支就是整套机制:正是它让这三个分类,以及任何在你编写客户端之后新增的分类,在到达时都不会导致你的客户端出错。控制流应基于粗粒度分类进行分支;当你需要具体原因时,再读取 `422` 响应体中每个字段对应的 `type`。 + +**状态:尚未实现。** 这三个分类各自对应 Router 尚不具备的行为,而且每个分类都会在开始输出它的同一次变更中加入词汇表,绝不会在此之前加入。 + +## Router 并不覆盖每个合作伙伴操作 + +Router 运行合作伙伴的*模型*。它并不承接合作伙伴暴露的每个操作:文件上传、账户与资产读取、提供商特定的管理调用、流式聊天端点,以及某些合作伙伴发布的提交与轮询配对。Router 也不会重塑其中任何一项:它转发模型的原生输入,并原样返回其原生输出,因此不存在可将不受支持的操作移植上去的统一封装格式。 + +**替代做法。** `/proxy/…` 下的合作伙伴代理路由在同一主机上、使用同一凭证仍然完全受支持,它们正是 Router 未覆盖的任何内容的答案。它们没有被弃用,也没有处于逐步淘汰的路径上;在同一集成中与 Router 一起使用它们属于预期用法,而非变通方案。当你想跨许多模型只使用一种路由形状和一个凭证时,请使用 Router;当你需要某个特定的合作伙伴操作、提供商自身的流式响应,或 Router 刻意隐藏的提交与轮询控制时,请使用 `/proxy/…`。 + +**状态:有意为之。** Router 刻意收窄了暴露面:单一路由形状本身就是其特性。代理暴露面保持现状。 + +## 下一步 + +- [Comfy Router 快速入门](/zh/api-reference/comfy-router/quickstart): 在 Python 或 TypeScript 中发起第一个可运行的调用。 +- [Comfy Router API 参考](/zh/api-reference/comfy-router/reference): 涵盖 Comfy Router 发送的每一个端点、每一个参数和每一个错误类别。 diff --git a/zh/api-reference/comfy-router/quickstart.mdx b/zh/api-reference/comfy-router/quickstart.mdx new file mode 100644 index 000000000..27c9e98a1 --- /dev/null +++ b/zh/api-reference/comfy-router/quickstart.mdx @@ -0,0 +1,306 @@ +--- +title: "Comfy Router 快速入门" +description: "从零开始,大约五分钟内,使用 Python 和 TypeScript,在 Comfy Router 上生成一张图像。" +translationSourceHash: 1825ee75 +translationFrom: api-reference/comfy-router/quickstart.mdx +translationBlockHashes: + "_intro": fd4302ce + "Why this page uses `bfl/flux-2-pro`": 04e30793 + "Get a key": 447aa37e + "cURL": bc3e1e4c + "Python": 059e95b8 + "TypeScript": fed849f1 + "Reading the `422`": 6d59c614 + "Where the model's fields come from": 157d882f + "Next": ce5a29fb +--- + +**Comfy Router 尚未正式发布。** 以下路由:`POST /v1/models/{provider}/{model}` 及其目录与 schema 兄弟路由,目前均尚未处理请求:经过身份验证的调用目前会返回 `404`。本页面记录的是这些路由未来将提供的契约,并提前于该发布公开,以便集成可以据此进行编写。这不是对当前可执行行为的描述。 + + +Comfy Router 通过单一主机、单一凭据和单一路由形状来运行合作伙伴模型。本页面是通往已生成图像的最短完整路径:安装客户端、设置密钥、发送一个请求、读取结果,并在真正遇到第一次失败之前,先看清失败的样子。 + +Base URL 为 `https://api.comfy.org`。路由为 `POST /v1/models/{provider}/{model}`,请求体是模型自身的原生 JSON 输入,`200` 响应携带模型自身的原生 JSON 输出。Router 不会对两者进行包装,因此,只需更改主机,您已针对合作伙伴 API 写好的调用即可变成 Router 调用。 + +## 为什么本页使用 `bfl/flux-2-pro` + +`bfl/flux-2-pro` 在 p50 下大约 3.1 秒返回,这是 Router 上实测最快的路径,也是让五分钟内获得首个结果成为现实的原因。较慢的模型会把这段预算花在等待上,而不是阅读上。 + +这只是方便之选,并非必需。Router 上的所有其他模型都以完全相同的方式调用:相同的路由、相同的凭据请求头、相同的错误分类、相同的 `X-Comfy-Request-Id`。唯一会变的只有模型 ID、请求体中的字段,以及读回结果的形状。例如,Gemini 在 p95 下以 72.8 秒轻松完成。Router 会在整个生成期间保持连接,而不是返回一个任务句柄供轮询。没有边缘上限会将长调用截断,但 Router 确实会限制调用本身:其服务器截止时间(默认 10 分钟)是它保持连接的最长时间,超过之后会返回 `504` / `deadline_exceeded`,并且不收费。替换 ID,然后从该模型自己的 schema(如下)中读取其字段。 + +## 获取密钥 + +Router 使用 Comfy API 密钥进行身份验证。你可以在 [platform.comfy.org/profile/api-keys](https://platform.comfy.org/profile/api-keys) 创建一个,然后将其放入环境中。下面的两个示例都会读取 `COMFY_API_KEY`,并且都不接受以字面量形式传入密钥,因此复制粘贴的代码片段不会将你的凭据带入提交记录。 + +```bash +export COMFY_API_KEY="comfyui-..." +``` + + +在 **`X-API-Key`** 请求头中发送 `comfyui-` 密钥,而不是 `Authorization: Bearer`。 +这两个请求头会选择不同的验证器:`X-API-Key` 是唯一能读取 `comfyui-` 密钥的 +入站请求头,而 `Authorization` 中的值会被路由到 JWT 分支,非 JWT 令牌在 +该分支会直接返回 `401 Invalid token`,密钥根本不会被查找。 +(对于 Cloud/Firebase **JWT**,使用 `Authorization: Bearer` 是正确的,这也是 +已生成的 [API 参考](/zh/api-reference/comfy-router/reference) 中“bearer token”的含义。) + + +密钥按工作区分属,并携带该工作区的模型权益和信用余额。没有可用凭据的请求会返回 `401`,并带有 `X-Comfy-Error-Type: unauthorized`;工作区无法运行该模型的请求会返回 `403` / `forbidden`。 + +## cURL + +最短的调用方式,适用于脚本、冒烟测试以及直接复制粘贴到终端: + +```bash +curl https://api.comfy.org/v1/models/bfl/flux-2-pro \ + -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"}' +``` + +响应即为模型的原生输出,与下文示例读取到的内容完全一致。请求失败时,响应体携带错误信息,`X-Comfy-Error-Type` 响应头会标明错误类型;请保留任何后续需要查询的响应中的 `X-Comfy-Request-Id` 响应头。macOS 和 Linux 均自带 `uuidgen`;在 Windows 上,可使用 `New-Guid` 或任何 UUID 来源生成 Idempotency-Key。 + +## Python + +Requires Python 3.9+ and `httpx`: + +```bash +pip install httpx +``` + +Save as `quickstart.py` and run it with `python quickstart.py`: + +```python +import os +import uuid + +import httpx + +BASE_URL = os.environ.get("COMFY_ROUTER_BASE_URL", "https://api.comfy.org") +MODEL = "bfl/flux-2-pro" + +# Give the client headroom ABOVE Router's own server deadline (10 minutes by +# default) so a call that reaches the server bound comes back as a typed 504 +# with a request id rather than as an opaque client abort. The deadline bounds +# how long Router holds the connection, not whether the call is billed: if the +# provider completed the generation, it is billed either way. +READ_TIMEOUT_SECONDS = 660.0 + + +class RouterError(Exception): + """A Comfy Router failure, typed by its X-Comfy-Error-Type bucket.""" + + def __init__(self, response: httpx.Response) -> None: + self.error_type = response.headers.get("X-Comfy-Error-Type", "internal_error") + self.request_id = response.headers.get("X-Comfy-Request-Id") + self.status_code = response.status_code + # Parse defensively: an error can arrive as an HTML 502 from a load + # balancer, a plain-text 429, an empty body or a truncated JSON one. The + # status, the bucket and the request id above are the parts worth + # keeping, so a body that will not parse must not replace this exception + # with a JSONDecodeError and lose them. + body = None + if response.headers.get("content-type", "").startswith("application/json"): + try: + body = response.json() + except ValueError: + body = None + detail = body.get("detail") if isinstance(body, dict) else None + # A 422 carries a detail[] array - one entry per rejected field, each + # keeping its own `loc`, `msg` and `type`. Every other bucket carries a + # plain `detail` string. + self.errors = detail if isinstance(detail, list) else [] + self.detail = detail if isinstance(detail, str) else f"HTTP {response.status_code}" + super().__init__(self.detail) + + +def run(model: str, arguments: dict, idempotency_key: str) -> dict: + # Idempotency-Key makes a retry safe on a PAID call: Router replays the + # original response for 24h instead of dispatching (and billing) the + # provider a second time. Reuse the SAME key when retrying one logical + # call; generate a new one for a new call. + response = httpx.post( + f"{BASE_URL}/v1/models/{model}", + headers={ + "X-API-Key": os.environ["COMFY_API_KEY"], + "Idempotency-Key": idempotency_key, + }, + json=arguments, + timeout=httpx.Timeout(READ_TIMEOUT_SECONDS, connect=10.0), + ) + if response.is_error: + raise RouterError(response) + return response.json() + + +result = run( + MODEL, + {"prompt": "a red teapot on a windowsill, morning light"}, + idempotency_key=str(uuid.uuid4()), +) +# Router forwards each provider's native output unchanged, so this path is +# BFL's, not a Router envelope. Reading a different model means reading its own +# output shape. +print("image:", result["result"]["sample"]) + +# The first failure most callers hit: a field the model's input schema requires +# is missing, so Router rejects the request BEFORE any provider call - which is +# why a 422 is never billed. +try: + run(MODEL, {"width": 1024}, idempotency_key=str(uuid.uuid4())) +except RouterError as exc: + print(f"{exc.error_type} (HTTP {exc.status_code}), request id {exc.request_id}") + for entry in exc.errors: + print(" ", ".".join(str(p) for p in entry["loc"]), "->", entry["msg"]) +``` + +```text +image: https://.../out.jpeg +invalid_input (HTTP 422), request id 6f1c... + body.prompt -> Field required +``` + +## TypeScript + +Requires Node 18+ (for built-in `fetch`, `AbortSignal.timeout` and `crypto.randomUUID`) and `tsx` to run TypeScript directly: + +```bash +npm install --save-dev tsx +``` + +Save as `quickstart.mts` — the `.mts` extension is load-bearing, because the file uses top-level `await` and that needs an ES module — and run it with `npx tsx quickstart.mts`: + +```typescript +const BASE_URL = process.env.COMFY_ROUTER_BASE_URL ?? "https://api.comfy.org"; +const MODEL = "bfl/flux-2-pro"; + +// Headroom ABOVE Router's own server deadline (10 minutes by default), so a +// call that reaches the server bound returns a typed 504 with a request id +// rather than aborting locally at the same moment. The deadline bounds how +// long Router holds the connection, not whether the call is billed: if the +// provider completed the generation, it is billed either way. +const CLIENT_TIMEOUT_MS = 660_000; + +interface ValidationEntry { + loc: (string | number)[]; + msg: string; + type: string; +} + +/** A Comfy Router failure, typed by its `X-Comfy-Error-Type` bucket. */ +class RouterError extends Error { + readonly errorType: string; + readonly requestId: string | null; + readonly status: number; + /** A 422 carries a `detail[]` array — one entry per rejected field, each + * keeping its own `loc`, `msg` and `type`. Every other bucket carries a + * plain `detail` string. */ + readonly errors: ValidationEntry[]; + + constructor(response: Response, body: unknown) { + const detail = + typeof body === "object" && body !== null + ? (body as { detail?: unknown }).detail + : undefined; + super(typeof detail === "string" ? detail : `HTTP ${String(response.status)}`); + this.name = "RouterError"; + this.errorType = response.headers.get("X-Comfy-Error-Type") ?? "internal_error"; + this.requestId = response.headers.get("X-Comfy-Request-Id"); + this.status = response.status; + this.errors = Array.isArray(detail) ? (detail as ValidationEntry[]) : []; + } +} + +/** Read a body without letting a non-JSON error page mask the real failure. */ +async function parseBody(response: Response): Promise { + const text = await response.text(); + try { + return JSON.parse(text) as unknown; + } catch { + return undefined; + } +} + +async function run( + model: string, + args: Record, + idempotencyKey: string, +): Promise { + // Idempotency-Key makes a retry safe on a PAID call: Router replays the + // original response for 24h instead of dispatching (and billing) the provider + // a second time. Reuse the SAME key when retrying one logical call. + const response = await fetch(`${BASE_URL}/v1/models/${model}`, { + method: "POST", + headers: { + "X-API-Key": process.env.COMFY_API_KEY ?? "", + "Idempotency-Key": idempotencyKey, + "Content-Type": "application/json", + }, + body: JSON.stringify(args), + signal: AbortSignal.timeout(CLIENT_TIMEOUT_MS), + }); + // Branch on `ok` FIRST: an HTML 502, a plain-text 429 or an empty body must + // still surface the status, the bucket and the request id. + const body = await parseBody(response); + if (!response.ok) throw new RouterError(response, body); + return body as T; +} + +const result = await run<{ result: { sample: string } }>( + MODEL, + { prompt: "a red teapot on a windowsill, morning light" }, + crypto.randomUUID(), +); +// Router forwards each provider's native output unchanged, so this path is +// BFL's, not a Router envelope. Reading a different model means reading its own +// output shape. +console.log("image:", result.result.sample); + +// The first failure most callers hit: a field the model's input schema requires +// is missing, so Router rejects the request BEFORE any provider call - which is +// why a 422 is never billed. +try { + await run(MODEL, { width: 1024 }, crypto.randomUUID()); +} catch (exc) { + if (!(exc instanceof RouterError)) throw exc; + console.log(`${exc.errorType} (HTTP ${String(exc.status)}), request id ${String(exc.requestId)}`); + for (const entry of exc.errors) console.log(" ", entry.loc.join("."), "->", entry.msg); +} +``` + +```text +image: https://.../out.jpeg +invalid_input (HTTP 422), request id 6f1c... + body.prompt -> Field required +``` + +## 解读 `422` + +`422` 是最值得在第一次真正调用之前理解的一个错误,因为它是你引发的。它表示 Router 已根据模型自身的输入模式检查了你的请求体并拒绝了它:必填字段缺失、值超出边界、图像太小。该检查在任何提供商调用之前执行,因此 `422` 不会产生任何成本:没有合作伙伴支出,之后也无需解答计费疑问。它不同于 `400`,后者是请求级失败(格式错误的游标、无法读取的信封),而不是字段级失败。 + +其响应体是 fal/FastAPI 的 `detail[]` 形状:一个数组,每个违规字段对应一个条目,每个条目保留自己的 `loc`(字段路径)、`msg`、`type`(具体的提供商级原因:`missing`、`value_error`、`image_too_small`),以及原因带有边界时的 `ctx`。正是这种字段级粒度,使得上面的示例将数组作为数据保留,而不是将其扁平化到异常消息中。 + + +输入模式尚未创建的模型会解析为文档中所述的宽松回退方案,该方案接受任何 JSON 对象,因此它会转发请求体,而不会返回 `422`。上面的示例展示了模式存在后你需要处理的形状;请将 `422` 块视为错误路径,而不是对特定请求体的保证响应。 + + +该响应体本身不携带 `error_type` 字段,因此在 `422` 上,`X-Comfy-Error-Type` 响应头是*唯一*机器可读的类别。两个示例都正因如此才首先从响应头中读取该类别,这也使得一个错误类就足以涵盖 Router 可能返回的所有失败。 + +`X-Comfy-Request-Id` 出现在每个响应上,成功、`4xx` 和 `5xx` 均如此,并且是在支持请求中引用的 ID。两个示例都将其附加到异常中,而不是让你在启用响应头日志的情况下重新运行来找到它。 + +## 模型字段的来源 + +`prompt` 是 `bfl/flux-2-pro` 唯一必需的字段;`width`、`height`、`seed` 和 `output_format` 是您接下来会需要用到的字段。与其复述一份可能过时的字段列表,不如实时读取模型的 schema: + +```bash +curl -H "X-API-Key: $COMFY_API_KEY" \ + https://api.comfy.org/v1/models/bfl/flux-2-pro/openapi.json +``` + +这份文档与服务器校验您的请求时所依据的文档是同一份,以独立的 OpenAPI 文档形式提供,因此所发布的内容与所执行的内容不可能不一致。取任意模型 ID,在其调用路径后追加 `/openapi.json`,然后根据返回的内容进行生成。 + +## 下一步 + +- [Comfy Router API 参考](/zh/api-reference/comfy-router/reference):每个端点、每个参数,以及全部十五类错误。 +- [Comfy Router 限制](/zh/api-reference/comfy-router/limitations):Router 目前尚未支持的功能,以及可以使用的替代方案。 diff --git a/zh/api-reference/comfy-router/reference.mdx b/zh/api-reference/comfy-router/reference.mdx new file mode 100644 index 000000000..8a08378c9 --- /dev/null +++ b/zh/api-reference/comfy-router/reference.mdx @@ -0,0 +1,267 @@ +--- +title: "Comfy Router API 参考" +description: "每个 Comfy Router 端点、参数、响应体和错误分类,均由 Comfy API 契约生成。" +translationSourceHash: 1e8777df +translationFrom: api-reference/comfy-router/reference.mdx +translationBlockHashes: + "_intro": 0114a881 + "Endpoints": 6bc355f0 + "Error buckets": 04a58305 + "Response headers": 2a099bc2 + "Per-model input schemas": ae73e63b + "Schemas": a14d076e +--- + +{/* + GENERATED FILE -- DO NOT HAND-EDIT. + + Produced from the Comfy API contract by gen_router_reference.py. Edit the + contract and regenerate; an edit made here is overwritten by the next run and + is rejected by the drift gate in the meantime. +*/} + +Comfy Router 的规范路由,以模型 ID 寻址。 + +基础 URL:`https://api.comfy.org` + +以下每个端点均需身份验证。请发送 `Authorization: Bearer `。 + +## 端点 + +### `GET /v1/models` + +**列出 Comfy Router 可以运行的模型。** + +Comfy Router 的模型目录:`POST /v1/models/{provider}/{model}` 所接受的规范模型 ID 的一页。SDK 在冷启动时调用此接口以发现可运行的模型,`model_not_found` 的建议也来自同一目录,因此,此处列出的 ID 在调用时返回 404 会比单独任一失败更糟糕。这种一致是结构性的,而非承诺:条目的 `provider` 和 `model` 是调用路由的两个路径段,引用与该路由路径参数相同的 schema 组件,而 `id` 是这两个段用 `/` 连接的结果。 + +**参数** + +| 名称 | 位置 | 必填 | 类型 | 约束 | 描述 | +| --- | --- | --- | --- | --- | --- | +| `cursor` | query | 否 | [`RouterPageCursor`](#routerpagecursor) | `pattern: ^[A-Za-z0-9._~+/=-]+$`, `minLength: 1`, `maxLength: 512` | 不透明的分页游标。传入上一页的 `next_cursor` 以获取下一页;获取第一页时省略该参数。有关该值为何不透明以及此路由为何按游标而非偏移量分页,请参阅 `RouterPageCursor`。 | +| `limit` | query | 否 | integer | `maximum: 100`, `default: 20` | 单页返回的模型数量。超过声明最大值的值不在契约范围内,但此路由不会拒绝它们:它会改为提供最大值,且实际提供的页面大小会在响应的 `limit` 字段中回显,因此调用方始终能检测到钳制行为。请将最大值视为实际的页面步长:请求更多且假定自己收到更多的客户端会漏掉行。0 和负值同样会被接受并选择默认值,这就是为什么没有声明 `minimum`:小于 1 的值在此处是有意义的,而非无效。 | + +**响应** + +| 状态 | 响应体 | 响应头 | 描述 | +| --- | --- | --- | --- | +| `200` | [`RouterModelListResponse`](#routermodellistresponse) | `X-Comfy-Request-Id` | OK:模型目录的一页。 | +| `400` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | Router 请求级失败:请求从未到达模型,或因模型本身未反馈的原因而失败。响应体为 `RouterErrorResponse`,错误类别会在 `X-Comfy-Error-Type` 中重复。 | +| `401` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | Router 请求级失败:请求从未到达模型,或因模型本身未反馈的原因而失败。响应体为 `RouterErrorResponse`,错误类别会在 `X-Comfy-Error-Type` 中重复。 |### `GET /v1/models/{provider}/{model}` + +**按规范模型 ID 读取单个合作伙伴模型的目录条目。** + +单个 Comfy Router 模型的逐模型详情,调用方无需遍历整个分页目录即可查看某个模型。SDK 会在调用模型之前立即使用此端点来查找模型。 + +**参数** + +| 名称 | 位置 | 必填 | 类型 | 约束 | 描述 | +| --- | --- | --- | --- | --- | --- | +| `provider` | path | 是 | [`RouterProviderSegment`](#routerprovidersegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 64` | 规范 `{provider}/{model}[/{variant}]` 模型 ID 的小写提供商段:表示正在运行其模型的合作伙伴。 | +| `model` | path | 是 | [`RouterModelSegment`](#routermodelsegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 128` | 规范 `{provider}/{model}[/{variant}]` 模型 ID 的小写模型段:表示该提供商内要运行的模型。 | + +**响应** + +| 状态 | 响应体 | 响应头 | 描述 | +| --- | --- | --- | --- | +| `200` | [`RouterModelDetail`](#routermodeldetail) | `X-Comfy-Request-Id` | OK:该模型的目录条目。 | +| `404` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | Router 请求级错误:请求从未到达模型,或因模型自身未反馈的原因而失败。响应体为 `RouterErrorResponse`,错误类别在 `X-Comfy-Error-Type` 中重复。 |### `POST /v1/models/{provider}/{model}` + +**通过规范模型 ID 同步运行合作伙伴模型。** + +Comfy Router 的规范入口点,以模型 ID 寻址。请求体是合作伙伴模型自身的原生 JSON 输入,成功响应也是该模型自身的原生 JSON 输出:Router 原样转发两者,而非强加 Comfy 式封装,因此调用方只需更改主机即可在合作伙伴的 API 与 Router 之间切换。这是同步路径,与 `POST https://fal.run/{id}` 对应。响应携带已完成的结果。对应的队列端点 `/v1/queue/models/{provider}/{model}` 已在计划中,将把 fal 的 `fal.run` / `queue.fal.run` 拆分端点统一到同一主机上;该端点尚不属本契约的一部分。 + +**参数** + +| 名称 | 位置 | 必填 | 类型 | 约束 | 描述 | +| --- | --- | --- | --- | --- | --- | +| `provider` | path | 是 | [`RouterProviderSegment`](#routerprovidersegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 64` | 规范 `{provider}/{model}[/{variant}]` 模型 ID 的小写提供商段:即正在运行其模型的合作伙伴。 | +| `model` | path | 是 | [`RouterModelSegment`](#routermodelsegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 128` | 规范 `{provider}/{model}[/{variant}]` 模型 ID 的小写模型段:即该提供商内要运行的模型。 | + +**请求体** + +`application/json` -- [`RouterModelInput`](#routermodelinput)(必填) + +合作伙伴模型的原生 JSON 输入,原样转发给提供商。 + +**响应** + +| 状态 | 响应体 | 响应头 | 描述 | +| --- | --- | --- | --- | +| `200` | [`RouterModelOutput`](#routermodeloutput) | `X-Comfy-Request-Id` | OK:合作伙伴模型的原生 JSON 输出,原样返回。 | +| `403` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | Router 请求级失败:请求从未到达模型,或因模型自身未反馈的原因而失败。响应体为 `RouterErrorResponse`,错误分类会重复出现在 `X-Comfy-Error-Type` 中。 | +| `404` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | Router 请求级失败:请求从未到达模型,或因模型自身未反馈的原因而失败。响应体为 `RouterErrorResponse`,错误分类会重复出现在 `X-Comfy-Error-Type` 中。 | +| `422` | [`RouterValidationErrorResponse`](#routervalidationerrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | 请求已到达模型,但模型拒绝了其内容。响应体为 `RouterValidationErrorResponse`,即 fal/FastAPI 的 `detail[]` 形状,因此每个有问题的字段都保留自己的特定 `type` 和 `ctx`。`X-Comfy-Error-Type` 携带整个响应的粗粒度错误分类。 | +| `503` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | Router 请求级失败:请求从未到达模型,或因模型自身未反馈的原因而失败。响应体为 `RouterErrorResponse`,错误分类会重复出现在 `X-Comfy-Error-Type` 中。 | +| `504` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | Router 请求级失败:请求从未到达模型,或因模型自身未反馈的原因而失败。响应体为 `RouterErrorResponse`,错误分类会重复出现在 `X-Comfy-Error-Type` 中。 |### `GET /v1/models/{provider}/{model}/openapi.json` + +**以 OpenAPI 文档形式读取单个合作伙伴模型的输入模式。** + +单个 Comfy Router 模型的输入模式,以独立的 OpenAPI 文档形式提供,使调用方(SDK、代码生成工具或智能体)无需阅读 Comfy 的文字说明文档即可发现模型的启动参数。它与 fal 的逐模型模式端点相对应,并且是 SDK 快速入门所依赖的发现机制。 + +**参数** + +| 名称 | 位置 | 必填 | 类型 | 约束 | 描述 | +| --- | --- | --- | --- | --- | --- | +| `provider` | path | yes | [`RouterProviderSegment`](#routerprovidersegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 64` | 规范 `{provider}/{model}[/{variant}]` 模型 ID 中的小写提供商段:即其模型正在被运行的合作伙伴。 | +| `model` | path | yes | [`RouterModelSegment`](#routermodelsegment) | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 128` | 规范 `{provider}/{model}[/{variant}]` 模型 ID 中的小写模型段:即在该提供商内要运行的模型。 | + +**响应** + +| 状态 | 响应体 | 响应头 | 描述 | +| --- | --- | --- | --- | +| `200` | [`RouterModelInputSchemaDocument`](#routermodelinputschemadocument) | `X-Comfy-Request-Id`, `ETag`, `Cache-Control` | 成功:模型的输入模式,以独立的 OpenAPI 文档形式提供。 | +| `304` | - | `X-Comfy-Request-Id`, `ETag`, `Cache-Control` | 未修改:文档与调用方在 `If-None-Match` 中发送的 `ETag` 相比未发生变化。不返回响应体。 | +| `404` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | Router 请求级失败:请求从未到达模型,或因模型自身未反馈的原因而失败。响应体为 `RouterErrorResponse`,错误分类同样体现在 `X-Comfy-Error-Type` 响应头中。 | +| `500` | [`RouterErrorResponse`](#routererrorresponse) | `X-Comfy-Error-Type`, `X-Comfy-Request-Id` | Router 请求级失败:请求从未到达模型,或因模型自身未反馈的原因而失败。响应体为 `RouterErrorResponse`,错误分类同样体现在 `X-Comfy-Error-Type` 响应头中。 | + +## 错误分类桶 + +Router 失败的粗粒度机器可读分类桶,同时反映在 `X-Comfy-Error-Type` 响应头中,以便调用方无需解析响应体即可进行分支处理。该集合固定为十五个值:六个请求级分类桶 `invalid_input`、`content_policy_violation`、`provider_error`、`provider_timeout`、`insufficient_credits` 和 `model_not_found`,以及传输级 `unauthorized`、`forbidden`、`concurrency_limit_exceeded`、`client_disconnected`、`internal_error`、`deadline_exceeded`、`not_enabled`、`service_unavailable` 和 `rate_limited`。 + +### 请求级分类桶 + +针对 Router 已接受但随后无法完成的请求抛出。 + +| `error_type` | 含义 | +| --- | --- | +| `invalid_input` | 请求在到达模型之前就被拒绝:请求体格式错误、分页游标格式错误或已过期,或包含模型自身 schema 不接受的输入。 | +| `content_policy_violation` | 提供商基于内容政策理由拒绝了请求。该拒绝是确定性的:重新发送相同的输入仍会被拒绝。 | +| `provider_error` | 合作伙伴提供商报告了其自身的故障,或返回了 Router 无法解释为结果的响应。 | +| `provider_timeout` | 合作伙伴提供商未在其截止时间内应答。此分类桶表示提供商(PROVIDER)超时,绝不是 Router 自身的服务器截止时间,后者报告为 `deadline_exceeded`。二者同为 `504`,但被分开,因为它们指代不同的原因:前者表示合作伙伴失败,后者表示 Comfy 停止保持连接。 | +| `insufficient_credits` | 发起调用的工作区没有足够的积分来运行该模型。 | +| `model_not_found` | `{provider}/{model}` ID 指向 Router 无法运行的模型;未知提供商也归入此类。`detail` 最多携带三条建议,这些建议取自调用方有权查看的模型。 | + +### 传输级分类桶 + +由 Router 自身抛出,发生在调用模型之前或调用过程之中。 + +| `error_type` | 含义 | +| --- | --- | +| `unauthorized` | 请求未携带可用的凭据。 | +| `forbidden` | 凭据有效,但无权访问此模型或执行此操作。 | +| `concurrency_limit_exceeded` | 工作区已在进行中的调用数量已达到允许的上限;等待其中一项调用完成后重试。 | +| `client_disconnected` | 调用方在 Router 返回结果之前关闭了连接。该错误会被记录而非投递,因为已没有可写入的 socket。它属于归因,而非计费结果:提供商已完成的生成任务都会计费,无论调用方是否收到响应。 | +| `internal_error` | Router 自身失败。客户端也应当将任何无法识别的分类桶视为该值,这样以后再向集合中新增分类也不会破坏早先生成的客户端。 | +| `deadline_exceeded` | 在答案到达之前,Comfy 在自身配置的时限处停止了保持连接。它与 `provider_timeout` 同为 `504`,这一对值表明是哪一方超时;此值是 Comfy 自身的时限,因此请求没有任何部分被拒绝,可以重试同一请求。它不涉及费用:提供商已完成的生成任务都会计费,无论调用方是否收到响应。 | +| `not_enabled` | Comfy Router 尚未为该调用方启用。请求本身没有任何问题,模型也存在,这正是它不是 `model_not_found` 的原因。它与 `forbidden` 同为 `403`,但绝非同一回事:`forbidden` 是对调用方的权限判定,而此值是发布(rollout)的状态。它是终端(TERMINAL)状态:不要重试,也不要将其视为服务中断。 | +| `service_unavailable` | Comfy Router 依赖的某个服务暂时不可用,调用方没有任何过错。使用退避(backoff)策略重试:它是此处唯一一个条件会自动清除的分类桶,调用方无需更改请求,也无需释放并发槽位,这正是它与其它可重试响应(`concurrency_limit_exceeded`、`deadline_exceeded`)的区别。它与 `internal_error` 不同,后者是 `500`,表示 Router 自身失败,因此客户端可以区分“稍后再来”和“这次调用不会成功”。 | +| `rate_limited` | 调用方已用尽按窗口(WINDOW)计量的配额,必须等待该窗口滚动过去。它与 `concurrency_limit_exceeded` 同为 `429`,但并非同一回事:后者在调用方自己的某个进行中调用完成的那一刻即会解除,因此在几秒后重试是正确的;而前者无论调用方做什么都无法提前解除。`detail` 会指明该窗口。 | + +## 响应头 + +| 响应头 | 类型 | 描述 | +| --- | --- | --- | +| `Cache-Control` | 字符串 | 所提供的架构文档的新鲜度指令。`private` 是因为该路由经过身份验证:文档本身并不特定于调用方,但共享缓存不得保存对已认证请求的响应;`must-revalidate` 则用于让过期副本根据 `ETag` 重新验证,而不是继续将其提供出去。 | +| `ETag` | 字符串 | 基于所提供文档字节的强实体标签,用于 `GET /v1/models/{provider}/{model}/openapi.json`。每个模型的架构很少变更,而 SDK 会频繁重新获取它,因此调用方应存储此值,并将其作为 `If-None-Match` 发送回去,以获得 `304` 而不是整个文档。 | +| `X-Comfy-Error-Type` | [`RouterErrorType`](#routererrortype) | 失败原因的粗粒度、机器可读分类,由 Router 在每个错误响应上设置。其值与 `RouterErrorResponse.error_type` 相同;在 `422` 响应上,它是唯一的机器可读分类,因为该响应体是 fal/FastAPI 的 `detail[]` 形状,本身没有 `error_type` 字段。因此,客户端可以仅根据此响应头进行分支判断,再决定收到的是两种 Router 错误体中的哪一种。 | +| `X-Comfy-Request-Id` | 字符串 | 服务器为此次调用生成的标识符,存在于每个 Router 响应上:成功、4xx 和 5xx 响应均如此,因为错误响应恰恰是用户需要在支持请求中引用该 ID 的时候。相同的值会写入调用的使用/审计事件中,这使得关于费用的投诉可以直接关联到该费用本身,而无需按时间戳搜索。 | + +## 各模型的输入模式 + +模型自身的输入字段不在此处重复列出。请通过 `GET /v1/models/{provider}/{model}/openapi.json` 实时读取这些字段,该端点提供与服务器校验调用时所依据的同一份文档,因此对外发布的内容与强制执行的内容不会出现偏差。从 `GET /v1/models` 获取模型 ID,在其调用路径后附加 `/openapi.json`,然后根据返回的文档进行生成。 + +## 模式 + +### RouterChargesOnPolicyRejection + +模型因内容政策而拒绝的调用,是否仍会向调用方收费。各提供商的做法不同,这种差异在调用时不可见。用户看到同一次调用出现错误和扣费时,也无从事先知晓,因此该信息会在调用之前按模型逐一说明,而不是留给各提供商的口口相传。 + +类型:`string`### RouterErrorResponse + +Router 的请求级错误体:当请求从未到达模型,或因模型本身未反馈的原因(认证、配额、未知模型 ID 或提供商传输)而失败时返回的内容。模型级验证失败具有自身的形状 `RouterValidationErrorResponse`,因为将 FastAPI 的 `detail[]` 数组扁平化为此 `detail` 字符串会破坏 SDK 所依赖的逐字段粒度。 + +| 字段 | 类型 | 必填 | 约束 | 描述 | +| --- | --- | --- | --- | --- | +| `detail` | 字符串 | 是 | - | 失败的人类可读描述,可安全展示给最终用户。不可由机器解析。请改用 `error_type` 进行分支判断。 | +| `error_type` | [`RouterErrorType`](#routererrortype) | 是 | - | Router 失败的粗粒度、机器可读分类,镜像在 `X-Comfy-Error-Type` 响应头中,使调用方无需解析响应体即可进行分支判断。该集合固定包含十五个值:六个请求级分类 `invalid_input`、`content_policy_violation`、`provider_error`、`provider_timeout`、`insufficient_credits` 和 `model_not_found`,以及传输级分类 `unauthorized`、`forbidden`、`concurrency_limit_exceeded`、`client_disconnected`、`internal_error`、`deadline_exceeded`、`not_enabled`、`service_unavailable` 和 `rate_limited`。 |### RouterErrorType + +粗粒度、机器可读的 Router 失败分类,同时镜像在 `X-Comfy-Error-Type` 响应头中,调用方无需解析响应体即可进行分支处理。该集合固定为十五个值:六个请求级分类 `invalid_input`、`content_policy_violation`、`provider_error`、`provider_timeout`、`insufficient_credits` 和 `model_not_found`,以及传输级 `unauthorized`、`forbidden`、`concurrency_limit_exceeded`、`client_disconnected`、`internal_error`、`deadline_exceeded`、`not_enabled`、`service_unavailable` 和 `rate_limited`。 + +类型:`string`### RouterModelBilling + +调用方在发起调用之前需要了解的按模型计费的**事实**,而非价格。此处从不出现用量和费用数字。 + +| 字段 | 类型 | 必填 | 约束 | 描述 | +| --- | --- | --- | --- | --- | +| `charges_on_policy_rejection` | [`RouterChargesOnPolicyRejection`](#routerchargesonpolicyrejection) | 是 | - | 此模型因内容政策原因拒绝的调用是否仍会向调用方收费。不同提供商的处理方式不同,且调用时无法看出差异;用户看到同一调用既报错又被收费时,无从事先得知。因此这一点会在调用之前按模型说明,而不是留给各家提供商的惯例去猜测。 |### RouterModelDetail + +某个 Comfy Router 模型的逐模型详细信息:目录列表为其报告的所有内容,外加仅单模型路由才包含的逐模型字段。 + +由 [`RouterModelListEntry`](#routermodellistentry) 和 [`RouterModelDetailFields`](#routermodeldetailfields) 组成。 + +类型:`object`### RouterModelDetailFields + +`RouterModelDetail` 中目录列表并不包含的另一半:按模型区分的字段,值得单独查询一次,但无需在分页目录页面的每个条目上重复列出。 + +| 字段 | 类型 | 必填 | 约束 | 描述 | +| --- | --- | --- | --- | --- | +| `input_schema_url` | 字符串 | 否 | `format: uri`, `pattern: ^https://`, `maxLength: 2048` | 指向此模型输入模式文档的指针,该文档描述 `POST /v1/models/{provider}/{model}` 为此模型接受的请求正文。只有指针属于此契约的一部分:其指向的文档是单独编写的。当模型尚未编写任何模式时,此字段不存在。 |### RouterModelId + +Comfy Router 模型的规范 ID 为 `{provider}/{model}`,这正是 `POST /v1/models/{provider}/{model}` 上用于寻址该模型的值,因此调用方可以将其直接插入该路径,而无需从任何其他内容重新推导。其 `pattern` 由 `RouterProviderSegment` 和 `RouterModelSegment` 通过单个 `/` 连接而成,`maxLength` 为两者之和再加上该分隔符。 + +类型:`string`,`pattern: ^[a-z0-9]+([._-][a-z0-9]+)*/[a-z0-9]+([._-][a-z0-9]+)*$`,`maxLength: 193`### RouterModelInput + +合作伙伴模型的原生 JSON 输入文档,按原样转发给提供商。其具体形状由合作伙伴而非 Comfy 拥有,因此这是一个开放对象:Router 不会缩窄、重命名或重新封装这些字段。它是一个命名组件(绝不会是内联匿名对象),因为 ComfyUI 的规范驱动代码生成需要一个类来生成。 + +类型:`object`### RouterModelInputSchemaDocument + +一个独立的 OpenAPI 文档,描述单个 Comfy Router 模型的输入:即 `POST /v1/models/{provider}/{model}` 针对该模型接受的请求体。它正是 `GET /v1/models/{provider}/{model}/openapi.json` 所返回的内容。 + +类型:`object`### RouterModelListEntry + +Router 模型目录中的一条条目:可运行模型的身份标识,仅此而已。按模型的详情路由会复用这条相同的条目,而不是重新陈述,这正是名称使用 `...ListEntry` 而非 `...Summary` 的原因:目录条目的定义必须唯一。按模型的详情以及按模型的输入/输出模式(schema)各有独立的路由,因此此形状保持为调用方调用模型所需的最小信息。这是有意为之,因为这是 SDK 在冷启动时获取的负载。`id` 是 `provider` 和 `model` 以 `/` 连接而成;这两个字段也分别携带,以便调用方无需拆分字符串即可拼出调用路径。 + +| 字段 | 类型 | 必填 | 约束 | 描述 | +| --- | --- | --- | --- | --- | +| `id` | [`RouterModelId`](#routermodelid) | 是 | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*/[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 193` | 一个规范的 Comfy Router 模型 ID,`{provider}/{model}`:正是 `POST /v1/models/{provider}/{model}` 上寻址该模型所使用的值,因此调用方可以将其直接插入该路径,而无需从任何内容重新推导。其 `pattern` 是 `RouterProviderSegment` 和 `RouterModelSegment` 以单个 `/` 连接而成,`maxLength` 是两者之和再加上该分隔符。 | +| `provider` | [`RouterProviderSegment`](#routerprovidersegment) | 是 | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 64` | 规范的 `{provider}/{model}[/{variant}]` 模型 ID 中的小写 `provider` 段:也就是其模型正被寻址的那个合作伙伴。调用路由的 `provider` 路径参数和目录条目的 `provider` 字段都引用这同一个模式(schema),这正是保证所列出的 ID 与所接受的 ID 不会发生偏离的原因。 | +| `model` | [`RouterModelSegment`](#routermodelsegment) | 是 | `pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`, `maxLength: 128` | 规范的 `{provider}/{model}[/{variant}]` 模型 ID 中的小写 `model` 段:即在该提供商内要运行的模型。该模式(schema)同时被调用路由的 `model` 路径参数和目录条目的 `model` 字段引用,出于与 `RouterProviderSegment` 相同的防偏离原因。 | +| `billing` | [`RouterModelBilling`](#routermodelbilling) | 是 | - | 调用方在调用之前所需的按模型计费事实,而非价格。使用量和成本数字绝不会出现在此处。 |### RouterModelListResponse + +Router 模型目录中的一页。 + +| 字段 | 类型 | 必填 | 约束 | 描述 | +| --- | --- | --- | --- | --- | +| `data` | [`RouterModelListEntry`](#routermodellistentry) 的数组 | 是 | - | 此页上的模型,最多 `limit` 个。 | +| `has_more` | 布尔 | 是 | - | 此页之后是否还存在下一页。只要该值为真,就继续遍历;不要因为 `data` 数据较少或为空就推断目录已结束。 | +| `next_cursor` | [`RouterPageCursor`](#routerpagecursor) | 否 | `pattern: ^[A-Za-z0-9._~+/=-]+$`、`minLength: 1`、`maxLength: 512` | 指向 Router 列表的不透明游标。它由服务器生成,且只会被原样往返传递:它不是偏移量,不是模型 ID,不保证有序,也不会在目录重建后保持稳定。因此,解析游标、对游标进行递增,或将游标保留到超出其来源遍历范围之外,都不在约定范围之内。采用游标而非偏移量,是因为目录是一个不断变动的列表:当条目在遍历途中被添加或删除时,基于偏移量的遍历会静默地跳过或重复条目,而调用方无法察觉这一点。 | +| `limit` | 整数 | 是 | `minimum: 1`、`maximum: 100` | 实际返回的页面大小。请求的 `limit` 超过最大值时会被向下钳制到最大值,而不是被拒绝,因此该值可能小于你请求的值。请使用此数值进行分页,而不是使用你发送的数值;否则你会误以为收到了实际并未获取的记录。 |### RouterModelOutput + +合作伙伴模型的原生 JSON 输出文档,原样返回给调用方。其具体形状由合作伙伴而非 Comfy 定义,因此这是一个开放对象:Router 不会对字段进行收窄、重命名或重新封装。它是一个命名组件(绝不是内联匿名对象),因为 ComfyUI 的规范驱动代码生成需要一个类来生成。 + +类型:`object`### RouterModelSegment + +规范的 `{provider}/{model}[/{variant}]` 模型 ID 中的小写 `model` 段,表示要在该提供商内运行的模型。调用路由的 `model` 路径参数与目录条目的 `model` 字段共享此段,原因与 `RouterProviderSegment` 相同,即防止漂移。 + +类型:`字符串`,`pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`,`maxLength: 128`### RouterPageCursor + +Router 列表的一个不透明游标。它由服务器生成,且仅用于往返传递:它不是偏移量,不是模型 ID,不保证有序,也不会在目录重建后保持稳定。因此,解析游标、递增游标,或将游标持久化到超出其来源遍历的范围之外,均不在契约范围之内。之所以使用游标而非偏移量,是因为目录是一个动态变化的列表:在遍历过程中添加或删除条目时,基于偏移量的遍历会静默跳过或重复条目,而调用方无法察觉这种情况的发生。 + +类型:`string`(`pattern: ^[A-Za-z0-9._~+/=-]+$`、`minLength: 1`、`maxLength: 512`)### RouterProviderSegment + +规范模型 ID `{provider}/{model}[/{variant}]` 中的小写 `provider` 段,表示其模型正被寻址的合作伙伴。调用路由的 `provider` 路径参数和目录条目的 `provider` 字段均引用此同一模式,这正是确保所列 ID 与所接受 ID 不会发生偏离的原因。 + +类型:`string`。`pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$`,`maxLength: 64`### RouterValidationErrorContext + +某个 `RouterValidationErrorDetail` 的违规边界,由提供商原样传递。例如,`{"limit_value": 8}` 对应 `greater_than`,`{"min_width": 512}` 对应 `image_too_small`,或 `{"max_size_bytes": 10485760}` 对应 `file_too_large`。键集因提供商和错误类型而异,因此该对象刻意设计为开放对象。将其收窄为固定字段列表,或将其并入 `msg` 字符串,正是移植集成得以编译通过、却随后悄然丢失读取该边界的代码分支的方式。当错误类型不携带边界时,此字段缺失。 + +类型:`object`### RouterValidationErrorDetail + +以 fal/FastAPI 形式表示的单次模型级验证失败。`type` 携带的是提供商的特定原因,例如 `value_error`、`missing`、`image_too_small`、`unsupported_audio_format`、`greater_than`、`file_too_large` 等等,这是 `RouterErrorType` 的粗粒度分类所无法表达的细节层次。出于同样的原因,它是一个开放字符串而非 `enum`:提供商的词汇表跨越两个层级,约有 48 个值,并随提供商的发布周期增长,而不是我们的发布周期。未建模的值必须能够到达调用方,而不是在反序列化时失败。 + +| 字段 | 类型 | 必填 | 约束 | 描述 | +| --- | --- | --- | --- | --- | +| `loc` | 任意类型数组 | 是 | - | 指向出错字段的路径,最外层段在前,例如 `["body", "image_url"]` 或 `["body", "images", 0]`,其中整数表示对数组的索引。 | +| `msg` | 字符串 | 是 | - | 针对该单次失败的可读描述。 | +| `type` | 字符串 | 是 | - | 该失败的具体、机器可读原因,由提供商原样透传。类型化 SDK 的异常层次结构正是根据此值进行分支判断的;响应头中的 `error_type` 只是它的粗粒度分类。 | +| `ctx` | [`RouterValidationErrorContext`](#routervalidationerrorcontext) | 否 | - | 单个 `RouterValidationErrorDetail` 中被违反的约束值,由提供商原样携带,例如 `greater_than` 附带的 `{"limit_value": 8}`、`image_too_small` 附带的 `{"min_width": 512}`,或 `file_too_large` 附带的 `{"max_size_bytes": 10485760}`。键集合取决于提供商和错误类型,因此这里有意采用开放对象:若将其收窄为固定字段列表,或将其并入 `msg` 字符串,恰恰会导致移植后的集成能够编译通过,却静默丢失读取该约束值的分支。当错误类型不携带约束值时,此字段不存在。 | +| `input` | [`RouterValidationErrorInput`](#routervalidationerrorinput) | 否 | - | 出错时的输入值,原样回显,以便调用方无需从 `loc` 重新推导即可看到被拒绝的内容。它可以是任意 JSON 类型:字符串、数字、布尔、数组、对象或 null,因此此模式有意保持不限定类型,而不是收窄为对象。当提供商不回显输入时,此字段不存在。 |### RouterValidationErrorInput + +违规的输入值,原样回显,以便调用方无需从 `loc` 重新推导即可看到被拒绝的内容。可以是任何 JSON 类型:字符串、数字、布尔、数组、对象或 null。因此此 schema 特意保持无类型,而不是限定为对象。当提供商不回显输入时,此字段不存在。### RouterValidationErrorResponse + +Router 的模型级 `422` 响应体,采用 fal/FastAPI 形式:请求格式良好,足以到达模型,但模型拒绝了其内容。请注意,它本身不携带 `error_type` 字段,这正是响应中的 `X-Comfy-Error-Type` 标头的用途:客户端无需先判断收到的是两个 Router 错误响应体中的哪一个,即可从标头读取粗略的错误分类。 + +| 字段 | 类型 | 必填 | 约束 | 描述 | +| --- | --- | --- | --- | --- | +| `detail` | [`RouterValidationErrorDetail`](#routervalidationerrordetail) 的数组 | 是 | - | 请求中发现的每个验证失败,每个违规字段对应一个条目。 | diff --git a/zh/api-reference/v2/overview.mdx b/zh/api-reference/v2/overview.mdx index 994ad68b2..142f1cf9f 100644 --- a/zh/api-reference/v2/overview.mdx +++ b/zh/api-reference/v2/overview.mdx @@ -1,7 +1,7 @@ --- title: "Comfy API v2 概览" description: "官方 Comfy API v2 参考:从外部应用上传输入、提交工作流任务并轮询获取结果,在 ComfyUI 中运行工作流。" -translationSourceHash: c3d19ef0 +translationSourceHash: 0a5bed89 translationFrom: api-reference/v2/overview.mdx --- @@ -35,3 +35,7 @@ translationFrom: api-reference/v2/overview.mdx |----------|-------------| | 资产 | 基于内容寻址 blob 的 UUID 标识记录。上传输入,下载输出。 | | 任务 | 工作流的一次执行。持久、可轮询、可取消。 | + +## Comfy Router + +Comfy API v2 通过提交并轮询的持久任务来运行工作流。如需直接调用模型(单个合作伙伴模型、单个请求、模型的原生输入和输出),请改用 [Comfy Router](/zh/api-reference/comfy-router/quickstart)。