One id, one material — on every printer, exactly once.
A filament_id names a material family: one commercial product line, shared by all of its per-printer variants. Devices match spools by filament_id + printer compatibility. That only works if, for any one printer, at most one compatible preset carries a given id. Bambu keeps this invariant; the other 64 vendor bundles broke it 1,256 times.
-f, tree-wide)Every number in this document is reproducible: an independent loader-faithful audit re-derives the validator's output exactly (1256 = 1256), and every code claim carries a file:line reference.
The plan in one screen
Two deliverables: a generation rule so ids can never become ambiguous again (goal 1), and a migration that fixes the 356 existing collisions without breaking users (goal 2).
Every ecosystem funnels through this one id
Not just Bambu. Five device ecosystems put a filament_id into the same pipeline; the slicer then picks the single preset that matches the id and is compatible with the active printer.
The invariant, drawn
✓ Correct — Bambu PLA Basic, id GFA00
| printer preset | compatible presets with GFA00 |
|---|---|
| X1C | …@BBL X1C 1 |
| X1C 0.2 nozzle | …@BBL X1C 0.2 nozzle 1 |
| P1P | …@BBL P1P 1 |
Variants share the id; their compatible_printers are disjoint. Each printer sees the id exactly once.
✗ Broken — Qidi, id GFB99 (real case)
| printer preset | compatible presets with GFB99 |
|---|---|
| X-Max 3 0.4 nozzle | Bambu ABS · HATCHBOX ABS · Overture ABS · PolyLite ABS · QIDI ABS Rapido · QIDI ABS-GF · QIDI ASA · ASA-Aero · PC-ABS-FR · Generic ABS … 16 |
Sixteen different materials answer to one id on one printer. A spool tagged “generic ABS” matches
whichever preset sorts first — and GFB99 was stamped into 317 Qidi files.
Two consumers you can't see from the profiles
Most matching is printer-scoped, but several code paths match globally, by id alone — tray display name,
temperature_vitrification warnings, calibration history, and the multi-nozzle grouping key
FilamentGroup.cpp:513. So two different materials must never share an id even across
vendors: the first preset in the whole installed collection supplies the name, type and temperature data.
Seven schemes, zero rules, one epidemic
The only guidance that ever existed was “≤ 8 characters”, checked for BBL only. Every other convention was one contributor's invention, merged without comment, then imitated.
How it got here
Beyond the validator's reach
The -f check compares a printer only against its own vendor's filaments. Three further ledgers exist:
Nobody invents ids. Structure carries the rest.
Chosen by a 3-design → 2-judge → 3-attacker adversarial process. Deterministic minting won on ambiguity-prevention, contributor simplicity and enforceability; every “breaks” finding from the attack pass is folded in below.
The one-question test
Minted ids — the setting_id precedent, applied to families
Author workflow is two lines: commit the family with no id anywhere; run
python scripts/assign_filament_ids.py (or read the id CI prints in its failure message and paste it).
A --mint "<Vendor>/<Family>" one-shot prints an id without touching the tree.
Structure: the id lives in exactly one place
Reserved namespaces — never mint or hand-write into
Eleven patterns explain all 356 groups
Classified group-by-group against the actual profile files, then independently spot-checked (1 substantive disagreement in 14 samples, corrected). The task's two known patterns cover about half; the rest are new findings.
Fixes across all groups: 319 × mint a fresh id · 21 × trim compatible_printers ·
21 × split compat-or-id · 5 × re-point inherits · 1 × merge. Which preset keeps a contested id follows a
mechanical precedence: Bambu-catalog material → OFL generic family → historical first owner.
The id came along when a profile was copied. Qidi stamped GFB99 into 317 files; Peopoly put PLA-Silk's id on ABS.
A product line never got its own @base — every variant inherits another family's id.
Flashforge's umbrella collapses ~140 presets into one id.
inherits. Never delete the old
shadow files — convert them (they carry real config).A vendor-branded filament rides a generic family id via inheritance, colliding with the true generic on the same printer.
Same material; the broad variant claims a printer that a dedicated variant covers — the BBL H2DP pattern from PR #14459.
compatible_printers. Ids untouched.The id sits on a shared settings template (fdm_filament_*), so every family that inherits the template collapses onto it (Prusa, Ginger Additive).
One OFL id spans several materials — and OFL presets are visible on every printer. OGFL06 = eSUN PLA-Marble and Fiberon PETG-ESD.
A vendor tunes a generic but renames it; alias shadowing fails and the OFL preset resurfaces beside it (Snapmaker PolyTerra J1 PLA).
The opposite failure: every nozzle variant has its own id (Prusa, SeeMeCNC, Afinia _##). No ambiguity — but devices can't recognize the material across nozzles.
Ids with spaces ("GFPLA Silk"), 11-char ids, invented GF-shaped ids colliding with Bambu's real allocations.
Same id, different material, different vendors — feeds wrong name/type/temperature data to the globally-matching consumers (§1).
Snapmaker “Benchy” demo presets coexist on purpose (gated by compatible_prints, which the id check
can't see); one preset lists the same printer three times.
Fresh ids only, family-atomic, names never change
The persistence analysis makes one strategy provably safe — and everything else forbidden.
What survives an id change
| surface | behavior across releases | verdict |
|---|---|---|
| User presets that inherit a system preset | filament_id re-derived from the parent on every load Preset.cpp:1658-1682 | safe |
| Saved 3mf projects | presets resolve by name + config equality, never by id Preset.cpp:2490-2576 | safe |
| Klipper / Creality / Snapmaker sync | tray ids derived at runtime from the installed bundle | safe |
| User root custom presets | copied a system id at creation and persist it forever — AMS auto-match falls back to generic-by-type | low residue |
| Qidi QD_* ids | encode the device protocol QidiPrinterAgent.cpp:146 — an exact-match contract | frozen |
| BBL GF* ids | live on RFID tags and printer-side records; hardcoded k-values in C++ | frozen |
| Recycling an old id for a different material | stale ids in user roots / old 3mfs would silently match the wrong material | forbidden, forever |
| Renaming / deleting preset names | user presets whose inherits no longer resolves are dropped at load Preset.cpp:1687-1691 | forbidden (use renamed_from if unavoidable) |
Phases
Tooling + snapshot — no profile changes
Land scripts/assign_filament_ids.py, the extended CI checks in ratchet mode, the validator OFL
cross-check and the runtime warning. Generate filament_id_snapshot.json (id→families multimap over main)
and an empty retired_ids.json; both are checked in and append-only.
OrcaFilamentLibrary first
OFL is the base bundle every vendor resolves against. Fix its 16 internal collisions and give Generic PETG HF/PETG-CF/PP-CF/… @System their own family roots instead of collapsing into their parent generic. Unambiguous ids stay byte-identical.
Per-vendor PRs, worst first — each widens the CI ratchet
Qidi → Flashforge → Elegoo → Prusa → Cubicon → Anycubic → InfiMech → Snapmaker → long tail (22 vendors,
mostly 1–8 one-line fixes). Each merged PR appends its vendor to the workflow's -f -v scope
(today: -v BBL -f); when all are in, drop -v and run tree-wide.
Delete the allowlists
Once tree-wide zero holds, the ratchet allowlists disappear and every check becomes a hard rule for anything born after the snapshot.
Vendor observations show these are systematic mistakes, not hundreds of independent bugs: Qidi is one blanket-stamp in three profile generations; Elegoo is a single inherit-the-class-base habit; Sovol is fixed by deleting six wrong lines.
The migration script's contract (hardened by the adversarial pass)
| rule | why |
|---|---|
| Re-mint everything — id literals in analysis notes are ignored; only group membership, fix category and keep-id precedence are consumed. Assert no emitted id matches ^(GF|QD_|P[0-9A-Fa-f]{7}$). | ~40% of classification notes suggested ids in the old invented-GF culture; CI would reject them. |
| Family-atomic — re-idding any preset re-ids every same-family sibling in the same commit, even outside the collision group. | Otherwise a family splits across two ids (FlyingBear GFB99 @S1 vs @Ghost7) — worse than the bug being fixed. |
| Bounded diff — a vendor PR may only touch collision-group files + same-family siblings; every id that is unambiguous today stays byte-identical. | Makes each PR mechanically auditable. |
| Config-equivalence gate — flattened effective config of every preset must be identical before/after, except the prescribed id/inherits/compat edits. | Makes structural conversions provably behavior-neutral (Flashforge's id-less shadow files become named family roots carrying their config byte-for-byte). |
No deletions, no renames — redundant twins are re-minted, not dropped; consolidation via renamed_from is a separate human-reviewed cleanup. | Deleting a name silently drops users' derived presets at load. |
Ratchets, not absolutes
The grandfathered landscape keeps today's benign sharing legal; the checks forbid anything new. All checks import the same mint function — the setting_id precedent.
| # | check | where |
|---|---|---|
| 1 | Format — valid iff OF[0-9A-Za-z]{6} or in the snapshot or vendor==BBL or QD_* in Qidi. The legacy set is closed. | orca_extra_profile_check.pydrop BBL-only gate :292, OFL skip :601 |
| 2 | Uniqueness ratchet — tree-wide id→families multimap; no id may gain a family claim not recorded in the snapshot (shared_catalog is the sanctioned exception). | |
| 3 | Structure ratchet — id key only on roots; every instantiated preset resolves an effective id (simulated loader walk); all family members resolve identically; no preset may override its inherited id (the Generic SBS drift class). | |
| 4 | Mint conformance — a new id must equal the mint (or a salt iteration); the failure message prints the expected value so hand-editors can paste it. | |
| 5 | Stability — an (id, family) pair on main may not change or vanish, following renamed_from chains, unless listed in a maintainer-gated migrations file. Retired ids go to an append-only ledger and are never redefined. | |
| 6 | Alias hygiene — a preset inheriting an OFL Generic * @System (no own id in its chain) must keep the OFL base name and non-empty compatible_printers; the error names the rename as the cause. | |
| 7 | OFL cross-check — extend check_duplicate_filament_subtypes to include OFL presets in every vendor's per-printer check, minus alias-excluded ones. m_excluded_from is already populated in validator context, so the 32 shadowed pairs won't false-positive and the 10 live ones are caught. Widen CI's -f per vendor. | C++ validator PresetBundle.cpp:5654 · :2302 |
| 8 | Runtime backstop — log a warning when the AMS-sync find_if sees 2+ compatible presets for one id. The only layer that can see side-loaded and forked bundles. One line. | runtime PresetBundle.cpp:3132/3233 |
Plus: the rule document ships in-repo (doc/developer-reference/filament_id.md) so CI messages have a stable link, and the profile-PR template gets one checkbox: “new materials: no filament_id key anywhere — CI prints the minted id.”
How this was verified
The analysis was run as a 52-agent pipeline over the real tree and real git history, with an adversarial design phase — findings were attacked before being adopted.
Amendments that came out of the attack pass: family identity declared on roots (not name-derived —
Afinia PLA@HS has no space before @); multi-root families legalized; checks 2–3 as
ratchets, not absolutes; the no-deletion rule; shadow-file conversion instead of deletion; classification id
literals quarantined; diameter siblings; case-insensitive reservation of the user id space.
Key discoveries that reshaped the design (with sources)
· The “26 Flashforge presets with no id” were a false alarm — the loader resolves them through the OFL base-bundle fallback to OGFL99/OGFG99 PresetBundle.cpp:4904-4909. The real bug is ~10 different Flashforge PLA products sharing one id.
· A missing id can never ship: it's a hard load error that discards the whole vendor bundle PresetBundle.cpp:5072, :5141.
· Qidi's QD_* ids are a device-protocol contract, discovered in QidiPrinterAgent.cpp:146-152 — a second frozen namespace nobody had documented.
· User-custom ids occupy P + 7 hex CreatePresetsDialog.cpp:533, and "null" is a sentinel — both reserved.
· OFL shadowing is keyed by alias, not id Preset.cpp:3684-3714, and is already active in validator context PresetBundle.cpp:2302 — which is what makes check #7 precise.
Five calls that are yours to make
Everything else in the plan holds under any of these answers. → marks the recommendation.
compatible_prints gating — more machinery for 3 presets.renamed_from) — restores shadowing.