mirror of
https://github.com/Comfy-Org/comfy-python-sdk.git
synced 2026-11-04 23:37:47 +00:00
[PR #202] [MERGED] feat(models): add models.list() and models.schema() for Router model discovery #203
Labels
No labels
audit:ai-readiness
audit:community
audit:github
audit:health-scoring
audit:policy
audit:release
audit:supply-chain
effort:medium
effort:small
effort:small
effort:trivial
priority:nice-to-have
priority:nice-to-have
priority:recommended
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Comfy-Org/comfy-python-sdk#203
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/Comfy-Org/comfy-python-sdk/pull/202
Author: @mattmillerai
Created: 9/29/2026
Status: ✅ Merged
Merged: 10/3/2026
Merged by: @mattmillerai
Base:
main← Head:matt/be-17771-models-list-schema📝 Commits (4)
90c72f6feat(models): add models.list() and models.schema() for Router model discoveryaf766b5fix(models): harden discovery parsing per review3ceef4eMerge origin/main into matt/be-17771-models-list-schemacd65888docs(transport): drop the etag clause from post_model_run/post_model_submit docstrings📊 Changes
10 files changed (+1402 additions, -9 deletions)
View changed files
📝
CHANGELOG.md(+12 -0)📝
README.md(+81 -6)📝
src/comfy_low/transport.py(+161 -0)📝
src/comfy_sdk/__init__.py(+7 -0)➕
src/comfy_sdk/model_catalog.py(+388 -0)📝
src/comfy_sdk/models.py(+100 -0)📝
tests/conftest.py(+97 -1)➕
tests/test_models_discovery.py(+498 -0)📝
tests/test_router_spec_contract.py(+54 -2)📝
tests/test_sync_async_parity.py(+4 -0)📄 Description
ELI-5
The Python SDK could run a Comfy Router model but couldn't tell you which models exist or what inputs a model takes. You had to write the HTTP calls yourself. This adds
client.models.list()(walk the model catalog) andclient.models.schema(model)(fetch a model's input/output OpenAPI document). Both work the same way as in the TypeScript SDK.What changed
models.list(*, cursor=None, limit=None, timeout=DISCOVERY_TIMEOUT)callsGET {router_base_url}/v2/models. It returns a lazyModelList:next_cursorwhilehas_moreis true, and yieldsCatalogModel(id, provider, model, billing)..page()returns oneModelPage(data, has_more, next_cursor, limit, request_id).limitis sent unchanged, including values above 100, because the server clamps them.AsyncModels.list()returns anAsyncModelList(async for,await ….page()).models.schema(model, *, etag=None, timeout=DISCOVERY_TIMEOUT)callsGET {router_base_url}/v2/models/{provider}/{model}/openapi.json. Each segment goes through the existingparse_model_idand is percent-encoded. It returnsSchemaResult(unchanged, document, etag, request_id).etag=, it sendsIf-None-Match, and a304returnsunchanged=True, document=Nonerather than raising.run(): Router base URL, bearer credential, retry policy (a429withRetry-Afteris ridden out), and error translation. Because a discovery read is a keyless GET that bills nothing, it also retries a5xxor read timeout (the possibly-in-flight class a run must opt into) whenever the client's policy retries at all.NO_RETRYis still one attempt. 401/403/404/5xx raise the typed Router exception read fromX-Comfy-Error-Type(Unauthorized,Forbidden,ModelNotFound,InternalError,ServiceUnavailable, …).comfy_low):get_model_catalog/get_model_schemaon both transports. The routes are confined to_MODEL_CATALOG_PATH/_MODEL_SCHEMA_PATH_TEMPLATE, andDISCOVERY_TIMEOUT = httpx.Timeout(30.0).tests/test_router_spec_contract.pypins both routes byget.operationId(listRouterModels,getRouterModelInputSchema) against the vendored spec, plus the catalog'scursor/limitparameter names. A mutation check (moving the constant to/v3/models) fails the test.tests/test_models_discovery.py(54 tests) drives the stub server. It covers:limit101/500 passed through304on anetagmatch, sync and async, and a stale tag getting the new document404→ModelNotFound, sync and async200, an entry whoseiddoesn't parse or doesn't match itsprovider/model) raisinginvalid_responsenext_cursorcleared on the last page, a304withoutETagkeeping the sent tag, and an empty or non-ASCIIetagrefused before any requestconftest.pygains the two routes.models.list/models.schemasections with runnable examples, and the hand-fetchopenapi.jsoninstruction now points atmodels.schema. There's also a CHANGELOG entry.Judgment calls
timeout=None, but it also asked for a 30 s default. In this SDKtimeout=Nonealready means "wait indefinitely" (run()), so the default isDISCOVERY_TIMEOUT(30 s) andNonekeeps its existing meaning.AsyncModels.listis notasync def: it returns the async iterable directly, soasync for m in client.models.list():works without an extraawait, matching the TypeScript shape. It is declared, with that reason, in_SYNC_ON_BOTHintests/test_sync_async_parity.py, which enforces the add-awaitcontract.has_morebut has nonext_cursor, or repeats a cursor already followed, raisesComfyError(code="invalid_response")instead of looping forever.304to a request that sent noetagis raised rather than reported as "unchanged". A caller with no tag has no cached copy that could be current.billingis carried as the decoded JSON mapping rather than a class, so new server fields reach callers without a release.id/provider/modelare validated because they addressrun():idmust parse as a run id and equal{provider}/{model}, so allowlisting onentry.providerand then runningentry.idis safe.Nonemeans only 304: the transport raisesinvalid_responsefor a200schema body that isn't a JSON object, so JSONnullcan't be read as "unchanged".CatalogModelhashes on its id triple (soset(client.models.list())works).SchemaResultstays unhashable because itsdocumentis a mutable dict.AGENTS.mdsays not to mockhttpx.ModelNotFoundon the schema route is the server's own404answer, translated. The SDK doesn't deny any capability locally. Malformed ids are refused locally withValueError, exactly asrun()refuses them.Verification against the live routes (read-only)
curlofhttps://api.comfy.org/v2/models?limit=2and…/v2/models/bfl/flux-2-pro/openapi.jsonboth return401withX-Comfy-Error-Type: unauthorizedand Router's error body. So both routes exist at the bound paths.client.models.list(limit=2).page()andclient.models.schema("bfl/flux-2-pro")both raiseUnauthorized(401,error_type="unauthorized",request_idpopulated).Merged with
mainMerged
mainat3ceef4eto pick up #145 (binary run results). The conflicts were in the README and insrc/comfy_sdk/models.py; both sides were kept. The "Two result shapes" section stays next tomodels.run, and its manualopenapi.jsonfetch now links tomodels.schema.AsyncModels.list/schemasit before #145's module__all__. Thecredits_usedspec-contract tripwire that failed on this branch earlier passes on the merged tree.Residual
The success path against a real deployment is unverified. No valid credential was available, so these were checked only against the stub server and the live
401answers:200page ofGET /v2/models200document withETagfromGET /v2/models/{provider}/{model}/openapi.json304onIf-None-MatchThe README examples (
client.models.list(),client.models.schema("bfl/flux-2-pro")) likewise weren't run end to end. Worth one authenticated smoke run, which is read-only and unbilled.scripts/check_drift.pydoes not pin the two discovery routes. It still checks only the run route. The pytest contract test above does pin them and runs in CI'stestjob, but thecodegen-driftjob won't flag a moved discovery route for someone who only runs the drift script.getRouterModel(GET /v2/models/{provider}/{model}) is still not exposed in Python (or TypeScript). It was out of scope by design; its only extra field,input_schema_url, is covered byschema().TypeScript parity allowlist: the TypeScript SDK's
surface-parity.test.tslistsmodelsMethodsAheadOfPython: ["schema", "list"]. That entry should be removed incomfy-typescript-sdkonce this merges.Docs code samples: adding Python/TypeScript
x-codeSamplesto the Router OpenAPI spec for the docs reference pages is separate work in the upstream contract.Stale docstring: the
comfy_sdk/models.pymodule docstring still says "runis the one model operation today". That was already stale before this change (submit/subscribe/handleexist) and I didn't rewrite it here.Provenance
3ceef4e(merge ofmain):ruff check .clean,ruff format --check .clean,mypy src0 issues in 22 files,pytest1135 passed / 9 skipped / 0 failed3ceef4e:scripts/check_drift.pyall OK;scripts/check_public_repo_hygiene.pyOKtests/test_models_discovery.pyon Python 3.10 (ataf766b5): 54 passedUnauthorizedtimeoutisDISCOVERY_TIMEOUT(30 s) rather thanNone, soNonekeeps meaning "wait indefinitely". The success path wasn't exercised against a live deployment (no credential); see Residual.🤖 Generated with Claude Code
Summary by CodeRabbit
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.