Skip to content

feat(architectures): derive the conditioning safe_globals from the registry - #30

Draft
Pfannkuchensack wants to merge 2 commits into
refactor/arch-variant-facetfrom
refactor/arch-conditioning-facet
Draft

feat(architectures): derive the conditioning safe_globals from the registry#30
Pfannkuchensack wants to merge 2 commits into
refactor/arch-variant-facetfrom
refactor/arch-conditioning-facet

Conversation

@Pfannkuchensack

Copy link
Copy Markdown
Collaborator

Stacked on #29#28#27. Base is refactor/arch-variant-facet; the diff shown here is
against it.

Fourth of the series from .ideas/Backend Modularization Plan.md. No behaviour changes.

Why this one matters most

ApiDependencies.initialize() passed ObjectSerializerDisk a hand-maintained list of thirteen
classes, right next to a comment in conditioning_data.py asking whoever adds a conditioning type to
remember to update it.

Forgetting did not fail at boot. torch.load refuses to deserialize a type that is not on the list,
so the error arrived mid-graph, on the first generation that happened to use the new
architecture. That is the worst failure mode in the whole inventory, and it is the one this facet
removes.

Reproduced, by omitting one class deliberately:

UnpicklingError: Weights only load failed. ...

No classes move

The original plan proposed relocating the *ConditioningInfo, *ConditioningField and
*ConditioningOutput classes into architecture packages. Dropped: their names are OpenAPI $refs
and appear in stored workflows, so a move is risk without benefit — and a missing Field or Output
class is already an ImportError that tests/test_imports.py catches. Only safe_globals fails
late, and registration alone fixes it.

Twelve types, fifteen architectures

SD1/SD2 share BasicConditioningInfo, SDXL and its refiner SDXLConditioningInfo, and FLUX.2
Klein reuses FLUXConditioningInfo
— same container, different encoders, with the pooled embedding
in clip_embeds and the Qwen3 sequence in t5_embeds.

The facet carries a bare type. There is no common base or protocol to bind to: SDXLConditioningInfo
extends BasicConditioningInfo, but the other ten are unrelated dataclasses. Their one shared
property — membership in ConditioningFieldData.conditionings — is asserted in CI instead, which is
also what keeps that hand-written union and the registry in step.

The tests exercise the mechanism, not just the list

Each of the twelve types is written through the same ObjectSerializerDisk the app uses and read
back, so a type missing from the derived allowlist fails the way it would in production — rather than
only failing a set comparison that could itself be wrong.

IPAdapterConditioningInfo stays out, with a test pinning that: it never reaches the serializer, it
lives on IPAdapterData, built and consumed inside a single denoise run.

invocation_api now exports all twelve rather than two, with a test tying the export list to the
registry. Custom nodes that build conditioning need them.

First widening of the defs allowlist

By one entry: conditioning_data. That module is close to a leaf — its only invokeai import is
regional_prompt_data, which imports nothing from invokeai at all — so a definition module cannot
reach back into the registry through it. This is exactly the visible, reviewable line the narrow
allowlist in #27 was designed to force.

One consequence worth naming. mypy now follows conditioning_data from the architectures package
and reports 23 pre-existing errors there, in a module that is not in pyproject's
follow_imports overrides. They are unrelated to this change — checking that file alone reports the
same 23 — so the package gate is now scoped with --follow-imports=silent, which still catches
errors in the architectures package itself. Verified by introducing one, then reverting it.

Verification

  • pytest tests/backend/architectures tests/app/util tests/test_imports.py tests/test_docs_json_export.py
    — 268 passed
  • mypy --follow-imports=silent invokeai/backend/architectures — clean
  • ruff@0.11.2 check . + format --check — clean
  • openapi.json regenerated and compared normalized — identical.
    docs/src/generated/invocation-context.json rebuilt via build_docs_bundle() — identical.

The local model database covers ten of the fifteen architectures (SD1, SDXL, FLUX, FLUX.2, Z-Image,
Krea-2, Qwen-Image, Wan, Anima, Ideogram-4), so a manual generation on any of those exercises the
derived allowlist end to end. SD2, SDXL-Refiner, SD3, CogView4 and ERNIE-Image have no local mains —
for those the round-trip test is the coverage.

🤖 Generated with Claude Code

…gistry

ApiDependencies.initialize() passed ObjectSerializerDisk a hand-maintained list
of thirteen classes, next to a comment in conditioning_data.py asking whoever
adds a conditioning type to remember to update it. Forgetting did not fail at
boot: torch.load refuses to deserialize a type that is not on the list, so the
error arrived mid-graph, on the first generation that happened to use the new
architecture. That is the worst failure mode in the whole inventory, and it is
the one this facet removes.

No classes move. The original plan proposed relocating the *ConditioningInfo,
*ConditioningField and *ConditioningOutput classes into architecture packages;
that was dropped. Their names are OpenAPI $refs and appear in stored workflows,
so a move is risk without benefit -- and a missing Field or Output class is
already an ImportError that tests/test_imports.py catches. Only safe_globals
fails late, and registration alone fixes it.

Twelve conditioning types serve fifteen architectures: SD1 and SD2 share
BasicConditioningInfo, SDXL and its refiner SDXLConditioningInfo, and FLUX.2
Klein reuses FLUXConditioningInfo -- same container, different encoders, with
the pooled embedding in clip_embeds and the Qwen3 sequence in t5_embeds.

The facet carries a bare `type`. There is no common base or protocol to bind to:
SDXLConditioningInfo extends BasicConditioningInfo, but the other ten are
unrelated dataclasses. Their one shared property -- membership in
ConditioningFieldData.conditionings -- is asserted in CI instead, which is also
what keeps that hand-written union and the registry in step.

The tests exercise the real mechanism rather than only comparing lists. Each of
the twelve types is written through the same ObjectSerializerDisk the app uses
and read back, so a type missing from the derived allowlist fails the way it
would in production. Verified by omitting one deliberately: torch.load raises
UnpicklingError, "Weights only load failed".

IPAdapterConditioningInfo stays out, with a test pinning that. It never reaches
the serializer -- it lives on IPAdapterData, built and consumed inside a single
denoise run.

invocation_api now exports all twelve rather than two, with a test tying the
export list to the registry. Custom nodes that build conditioning need them.

This is the first PR to widen the defs import allowlist, by one entry:
conditioning_data. That module is close to a leaf -- its only invokeai import is
regional_prompt_data, which imports nothing from invokeai -- so a definition
module cannot reach back into the registry through it.

That widening has one consequence worth naming: mypy now follows
conditioning_data from the architectures package and reports 23 pre-existing
errors there, in a module that is not in pyproject's follow_imports overrides.
They are unrelated to this change -- checking that file alone reports the same
23 -- so the package gate is now scoped with --follow-imports=silent, which
still catches errors in the architectures package itself. Verified by
introducing one.

openapi.json, schema.ts and docs/src/generated/invocation-context.json are
unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant