DESIGN REVIEW resources/profiles · filament_id

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.

id anatomy — Bambu's grammar GF A 00 prefix — Bambu's namespace frozen: AMS RFID tags carry these bytes family letter — A: Bambu PLA B ABS/ASA · C PC · G PETG · L generic/3rd-party PLA N PA · P PP/PE · S support · T PET/PPS · U TPU tier number 00–49 branded · 50–59 fiber-filled 60–70 partners · 95–99 generic, desc. shared by every variant: Bambu PLA Basic @BBL X1C Bambu PLA Basic @BBL P1P Bambu PLA Basic @BBL X1C 0.2 nozzle
1,256
printer-level ambiguity errors (validator -f, tree-wide)
356
logical collision groups after de-duplication
30 / 65
vendor bundles affected / total
0
violations in BBL — cleaned in PR #14459, CI-gated

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.

§0 overview

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).

the whole plan TODAY 7+ ad-hoc id schemes 356 collision groups zero written rules CI covers BBL only RULE + TOOLING §3 §6 deterministic OF* mint id lives on @base only CI ratchets + snapshot no profile changes yet MIGRATION §5 fresh ids only, ever OFL first, then per vendor names never change one reviewable PR each DONE invariant holds tree-wide, by construction BBL profiles are never touched · Qidi QD_* device ids are never touched · no preset is renamed or deleted
Why migration is safe — verified in code: user presets re-derive filament_id from their parent on every load Preset.cpp:1658-1682; 3mf projects resolve presets by name + config, not id Preset.cpp:2490-2576; Klipper, Creality and Snapmaker derive tray ids at runtime from the installed bundle. Fresh, never-reused ids on non-BBL system presets break nothing.
§1 how matching works

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.

runtime — device → preset BBL AMS from device RFID — frozen Qidi box QD_<series>_<v>_<t> — frozen Creality CFS scored at runtime — safe Klipper AFC/HH by filament type — safe Snapmaker color/type match — safe tray_info_idx (MQTT / agent) filament_ams_list["filament_id"] find_if( f.is_compatible && base(f)==f && f.filament_id == id ) Plater.cpp:3440 PresetBundle.cpp:3132/3233 → the ONE matching preset two matches? find_if silently returns whichever sorts first — no log, no warning; the AMS edit dialog even hides the second preset

The invariant, drawn

per printer × per id

✓ Correct — Bambu PLA Basic, id GFA00

printer presetcompatible 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 presetcompatible presets with GFB99
X-Max 3 0.4 nozzleBambu 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.

Two identity systems. The library (OFL) hides its global presets per printer by alias (name before  @) — Preset.cpp:3684 — while devices match by filament_id. When a vendor tunes Generic PLA but renames it Flashforge PLA Basic, the alias no longer matches, shadowing fails, and both presets are visible with the same id. The rule in §3 aligns the two systems: one family = one alias = one id.
§2 the landscape today

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.

who uses which id shape — 5,866 instantiated presets GF<letter><NN> (Bambu classic, mass-copied) 2,921 free-form ("GFPLA Silk", "LHF_pla", 36-char names…) 2,241 GF<BRAND><NNN> (Bambu partner ids) 395 O-prefixed (OGF* — OrcaFilamentLibrary mirrors) 211 GF<x>##_## (Afinia/Tiertime suffix) + other GF-shaped 43 + 55
Bambu's space, used outside Bambu invented, unowned deliberate Orca convention

How it got here

convention timeline 2025-01 OrcaFilamentLibrary created 2025-03 OGF* prefix born direct commit, no rationale 2025-05 “≤ 8 chars” check BBL-only · PR #9574 2025-06 _## suffix invented PR #9739, imitated since 2025-11 profile wiki removed its example taught copy-paste 2026-06 validator -f · BBL → 0 PR #14459 now this plan

Beyond the validator's reach

The -f check compares a printer only against its own vendor's filaments. Three further ledgers exist:

16
OFL-internal ids covering several materials — visible on every printer (e.g. OEPLAB00 = 14 Elegoo PLA products)
42
OFL×vendor same-id pairs — 32 already neutralized by alias shadowing, 10 live
67
ids meaning different materials in different vendors (GFU99 also covers a PEBA; Anycubic's GFL95 “Matte” ≠ Bambu's GFL95 “High Speed”)
§3 the rule

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

decision — same id or new id? Would a user call this a different spool product than anything already in the tree? YES — polymer, sub-brand, fiber-filled, 2nd diameter NO — same spool, tuned for another printer / nozzle it's a GENERIC material NEW FAMILY create «Family @base», not instantiated write NO filament_id anywhere run the mint script — or paste the id that CI prints for you JOIN THE FAMILY inherit the existing @base never write the filament_id key keep compatible_printers disjoint from the other variants JOIN THE OFL FAMILY inherit «Generic X @System» KEEP the «Generic X» name/alias (alias shadowing then hides the OFL preset on your printers) · no id key color → never a new id · “high-speed” for a different printer → same family “high-speed” selectable alongside the normal preset on one printer → new family second selectable diameter on the same printer → sibling family with its own id

Minted ids — the setting_id precedent, applied to families

id anatomy — the new namespace OF q3xT9k OF — Orca Family namespace disjoint from GF · QD_ · P· at two chars base62_6( uuid5( NS, "filament_family/<Vendor>/<Family>" ) ) e.g. filament_family/Elegoo/Elegoo PLA Matte → OFq3xT9k same derivation as setting_id · 8 chars total (AMS limit) same input → same id: no “next number” races · a fork mints the id upstream expects · CI recomputes and verifies renamed family? the id does not change — immutable once shipped; renamed_from covers the name

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

family shape Elegoo PLA Matte @base instantiation:false · filament_id: "OFq3xT9k" ← only here …@Centauri Carbon inherits @base · no id key compat: {CC 0.4} …@CC 0.2 nozzle inherits @base · no id key compat: {CC 0.2} …@Neptune 4 inherits @base · no id key compat: {N4, N4 Pro} disjoint compatible_printers = the PR #14459 invariant, by construction family identity is DECLARED — an optional "filament_family" key on the root overrides name derivation multi-root families are legal (Qidi's per-series bases) — every root must declare the identical id

Reserved namespaces — never mint or hand-write into

the id space, partitioned GF* Bambu AMS / RFID — frozen QD_* Qidi device — frozen P[0-9A-Fa-f]{7} · "null" user-custom presets OGF* + legacy shapes grandfathered, closed OF[0-9A-Za-z]{6} open — minted only Byte-copies of authentic Bambu ids stay legal where the checked-in shared_catalog sanctions the family (BBL, OFL, Fiberon). Everything already shipped and unambiguous is snapshot-frozen exactly as-is — the migration touches only colliding families.
Accepted trade-off: minted ids are opaque — OFq3xT9k doesn't say “PLA”. The family name sits in the same file; in exchange there is no registry, no “next free number” ceremony, no PR races, and forks mint the same id upstream expects. The mnemonic registry-grammar runner-up is preserved in §8 if you prefer it — everything else in this plan works under either format.
§4 error taxonomy — goal 2

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.

what's actually wrong — classified groups P1 copy_paste_id — different materials share an id verbatim 167 P2 wrong_inherits / id-less product line ~87 P3 generic_family_overlap — branded preset rides a generic id 68 P4 overclaim_compat — broader variant claims a dedicated printer 24 other — deliberate coexistence, hygiene, one true duplicate 21

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.

P1copy_paste_id~167

The id came along when a profile was copied. Qidi stamped GFB99 into 317 files; Peopoly put PLA-Silk's id on ABS.

fix — impostor families get minted ids on their roots; the owner keeps the id.
P2wrong_inherits / no root~87

A product line never got its own @base — every variant inherits another family's id. Flashforge's umbrella collapses ~140 presets into one id.

fix — create per-family roots with minted ids; re-point inherits. Never delete the old shadow files — convert them (they carry real config).
P3generic_family_overlap~68

A vendor-branded filament rides a generic family id via inheritance, colliding with the true generic on the same printer.

fix — branded → minted id. True generic tunings instead join the OFL family with matching alias. Sovol is the elegant case: just delete its wrong id lines and inherit OFL's.
P4overclaim_compat~24

Same material; the broad variant claims a printer that a dedicated variant covers — the BBL H2DP pattern from PR #14459.

fix — trim the broader preset's compatible_printers. Ids untouched.
P5template-carried idin P1/P2

The id sits on a shared settings template (fdm_filament_*), so every family that inherits the template collapses onto it (Prusa, Ginger Additive).

fix — move ids off templates onto family roots.
P6OFL-internal collision16 ids

One OFL id spans several materials — and OFL presets are visible on every printer. OGFL06 = eSUN PLA-Marble and Fiberon PETG-ESD.

fix — fix OFL first: it's the base bundle every vendor resolves against.
P7alias-mismatch re-exposure10 live

A vendor tunes a generic but renames it; alias shadowing fails and the OFL preset resurfaces beside it (Snapmaker PolyTerra J1 PLA).

fix — rename-to-alias where it's genuinely the same family, else mint.
P8per-variant ids~210 presets

The opposite failure: every nozzle variant has its own id (Prusa, SeeMeCNC, Afinia _##). No ambiguity — but devices can't recognize the material across nozzles.

fix — grandfather (they're unambiguous); converge opportunistically; document as anti-pattern.
P9format violationshygiene

Ids with spaces ("GFPLA Silk"), 11-char ids, invented GF-shaped ids colliding with Bambu's real allocations.

fix — disappears as a byproduct of re-minting; the unambiguous rest is snapshot-frozen.
P10cross-vendor semantic67 ids

Same id, different material, different vendors — feeds wrong name/type/temperature data to the globally-matching consumers (§1).

fix — mostly eliminated by P1–P3 re-minting; benign same-material generic sharing is legalized by the snapshot.
P11deliberate coexistence4

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.

fix — mint ids for demo presets; de-duplicate list entries; add a lint.
§5 migration plan

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

surfacebehavior across releasesverdict
User presets that inherit a system presetfilament_id re-derived from the parent on every load Preset.cpp:1658-1682safe
Saved 3mf projectspresets resolve by name + config equality, never by id Preset.cpp:2490-2576safe
Klipper / Creality / Snapmaker synctray ids derived at runtime from the installed bundlesafe
User root custom presetscopied a system id at creation and persist it forever — AMS auto-match falls back to generic-by-typelow residue
Qidi QD_* idsencode the device protocol QidiPrinterAgent.cpp:146 — an exact-match contractfrozen
BBL GF* idslive on RFID tags and printer-side records; hardcoded k-values in C++frozen
Recycling an old id for a different materialstale ids in user roots / old 3mfs would silently match the wrong materialforbidden, forever
Renaming / deleting preset namesuser presets whose inherits no longer resolves are dropped at load Preset.cpp:1687-1691forbidden (use renamed_from if unavoidable)

Phases

0

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.

1

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.

2

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.

3

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.

where the work is — collision groups per vendor Qidi97 Flashforge69 Elegoo37 Prusa31 Cubicon17 Anycubic14 InfiMech14 Snapmaker11 Artillery8 Creality8 FlyingBear8 Sovol6 18 more40 total (Volumic 6 · Ratrig 5 · Chuanying 4 · Dremel 3 · 4×2 · 10×1)

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)

rulewhy
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.
§6 enforcement

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.

#checkwhere
1Format — 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.py
drop BBL-only gate :292, OFL skip :601
2Uniqueness ratchet — tree-wide id→families multimap; no id may gain a family claim not recorded in the snapshot (shared_catalog is the sanctioned exception).
3Structure 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).
4Mint 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.
5Stability — 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.
6Alias 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.
7OFL 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
8Runtime 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.”

§7 evidence

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.

methodology 5 code analysts runtime · persistence · loader Bambu grammar · git history 38 classifiers every collision group, read from the actual profile files ground truth audit script = validator 1256 = 1256 · exact 3 designs deterministic mint registry grammar structure-only 2 judges both chose the mint 58 / 50 / 56 60 / 54 / 56 3 attackers 35 scenarios authors · runtime · migration mechanics every “breaks” → an amendment §3–§5 spot-check: 1/14

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.

§8 open decisions

Five calls that are yours to make

Everything else in the plan holds under any of these answers. marks the recommendation.

D1id format
Opaque deterministic mint OFq3xT9k — no registry, no races, fork-friendly; judges 2/2.
Mnemonic registry grammar <NS><FAM><SEQ> — readable ids, but someone allocates numbers forever.
D2multi-vendor brands
shared_catalog list — sanction authentic Bambu ids for byte-matching families (Snapmaker's Fiberon), low churn.
Hoist those families into OFL — cleaner long-term, more restructuring now.
D3demo presets
Mint ids for Benchy-style presets — simple, keeps the validator strict.
Teach the validator compatible_prints gating — more machinery for 3 presets.
D4the 10 live OFL duplicates
Rename-to-alias where it's truly the same family (with renamed_from) — restores shadowing.
Mint vendor ids everywhere — safer mechanically, more ids in the world.
D5where the rule doc lives
In-repo doc/developer-reference/filament_id.md — CI messages need a stable link.
Wiki-only — discoverable, but drifts from the checks that enforce it.
Suggested first step: land phase 0 (tooling + snapshot, zero profile changes). It is entirely additive, makes every later PR mechanically checkable, and turns the 356-group backlog into a shrinking allowlist that CI reports on every profile PR.