Files
civitai__civitai/docs/features/generator-model-onboarding.md
T
briant c60fd7c2e1 feat(models): import model files from Hugging Face server-side
A moderator pastes a Hugging Face repo URL at /moderator/huggingface-import, picks files, and our
servers fetch the weights into our storage — replacing a human downloading 20GB and re-uploading it
through the browser wizard. A finished import attaches to a ModelVersion as a ModelFile, which puts
it on the existing scan and hash pipeline.

The transfer is resumable by construction. A 20GB file cannot move inside one request or one job
run — jobs here hold a lock measured in minutes and a deploy rolls the pod — so it is a sequence of
independent parts: a ranged read from HF written as one multipart part. The row stores `uploadId`,
`partSize` and the parts written so far, and each cron run moves as many as fit in its budget. A
deploy costs one part, not the file.

Notes for review:

- Parts complete out of order, so the resume point is the SET of missing part numbers, never a
  count, and a resume addresses the bucket recorded on the row rather than whatever the backend
  config resolves to now.
- Every write inside a claimed run is fenced by `claimedBy`, and completion re-reads status from the
  PRIMARY first — without that, a cancel arriving during the final part still finalised the upload.
- `PART_SIZE_BYTES` is fixed at 16MB rather than `getUploadChunkSize`, whose 1000-part cap is a
  browser-presigning bound and would make part size (and so pod memory) grow with the file.
- Imported objects use the same bucket and the same `buildUploadKey` as a browser upload. Nothing
  about the import appears in the key; the HuggingFaceImport row is the index, because a key is
  immutable and a column is not.
- `official-model-admin` gains `hf-imports` and `attach-import`, so the upload step it used to hand
  back to a human is scriptable.

🔴 The migration has already been applied to production by hand; any other environment still needs
it. This repo never runs `prisma migrate deploy`.

🔴 Depends on `refactor/shared-upload-key`, which must merge first — this branch carries those four
files so it compiles, and they become no-ops on rebase.

Verified: typecheck, lint and prettier clean; 37 tests over the transfer engine, several
mutation-checked. Full suite 41,224 passed / 28 failed — all 28 pre-existing on main (9 verified
against a clean tree, 19 from a package.json/lockfile mismatch on @civitai/generation-metadata).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015cyrXpr87t9Tj3bnzRrUhp
2026-09-15 16:17:50 -06:00

9.4 KiB
Raw Blame History

Onboarding a model to the generator

How a new model gets from nothing to generatable. That covers a new ecosystem, a new CivitaiOfficial model, or a new version of an existing one. Mods test it on the live generator before anyone else can see it. The onboard-generator-model skill runs this flow end to end. This doc is what that skill follows, and the reference for doing any step by hand.

The flow

# Step Lands in Tool When
1 Ecosystem + base model records packages/civitai-shared/src/basemodel.constants.ts add-ecosystem skill new ecosystem or new base model
Deploy only if step 1 ran
2 Model, Draft, owned by CivitaiOfficial, with description — or the existing model's description updated for the new version model.upsert + moderator.models.transferOwnership write-model-description, then official-model-admin (create-model / update-description) once the user has approved the text always (review only if nothing changes)
3 Version, Draft, with its base model and usageControl modelVersion.upsert official-model-admin (create-version) always
4 Coverage row, then a cache bust "EcosystemCheckpoints", written through postgres-query --writable generation-coverage (add) always
5 Gate rule that hides it from non-mods Redis generation:gate-rules:by-id generation-gate-rules (add) or /moderator/generation-config always
6 Generation support, in both generator lanes constants, graphs, handlers add-generation-support always
7 Prompt-enhancement guide orchestrator prompt-analysis service add-prompt-enhancement-guide new ecosystem (image/video only)
Deploy, then mods test on the live generator generator-launch (check)
8 Launch: publish, then remove the gate a human, following generator-launch (launch)

Every tool in the last column is a skill. onboard-generator-model runs them in this order.

For a new version of an existing model, step 2 is a review of the live description with write-model-description. In every case, the user approves the exact text before it is written: official-model-admin shows the full HTML (or a diff for an update) and refuses to write unless it gets the approval hash of that text.

Why a new base model takes two deploys

modelVersion.upsert checks baseModel against the base-model names compiled into the server that handles the request (baseModelSchema in src/server/schema/model-version.schema.ts). So a version with a brand-new base model can't be created until the constants that name it are in production.

Step 6 hardcodes the new version's ID: it goes in the ecosystemSettings default model and in each graph's defaultModelId. So step 6 can't be written before step 3. The minimum is step 1, deploy, steps 26, deploy. A new model or version on an existing base model needs one deploy.

Step 1 on its own adds nothing to the generator. An ecosystem enters the picker only through its ecosystemSupport generation entry and a workflow list in config/workflows.ts, and both are part of step 6. The base-model record can still show up in upload and filter pickers. Set hidden: true on it if that is too early.

How a mod tests an unpublished version

This needs no special mechanism. Two existing rules combine:

  • Coverage. The first branch of the GenerationCoverage view covers any version listed in "EcosystemCheckpoints", and it does not check status. The view is a plain view, so the row takes effect as soon as it is inserted. resourceDataCache sits in front of it with a one-hour TTL, so bust the cache with modelVersion.bustCache after inserting.
  • Access. getResourceCanGenerate in src/server/services/generation/generation.service.ts treats Draft as a generatable status. It lets moderators and the model owner through the private check and refuses everyone else.

The result: a Draft version with an EcosystemCheckpoints row can be generated by moderators and by CivitaiOfficial, and by nobody else. Ideogram 4 was in this state for testing (version 3246186).

Why the gate rule is still needed

Draft stops non-mods from generating with the version, but it doesn't stop them from seeing it. Once step 6 deploys, the ecosystem is in everyone's picker, and a locked default checkpoint or a versions.options entry lists the version. A non-mod who selects it fails at submit. The gate rule removes it from view:

  • new ecosystem: gate the ecosystem key (EcosystemRecord.key, case-sensitive)
  • new version in an existing ecosystem: gate the model version ID

Use availableTo: moderators with presentation: hidden, so mods keep access and everyone else never sees the item. Add the rule before the deploy. A rule that names an ecosystem or version the running code doesn't know yet has no effect, so adding it early is safe. The rules model is described in generation-gating-rules-model.md.

A hidden rule also blocks canGenerate site-wide for everyone it gates. A version published while its rule is still in place stays ungeneratable for non-mods, including from its model page.

Two version shapes

The production EcosystemCheckpoints rows show two shapes of CivitaiOfficial version:

Shape usageControl Files Examples
API-only: the provider hosts the model ExternalGeneration none Qwen 3, Seedance 2.5, Flux 3 Video, Muse Image
Hosted weights: runs on our cluster Download uploaded in the version wizard Ideogram 4.0, MiniMax H3, LTXV 2.5, Mage Flow

MiniMax H3 appears in both rows, one version each, so the base model alone doesn't decide the kind. File count doesn't decide it either: a few older ExternalGeneration versions carry files that are never used. The signals that do help:

  • The handler's engine. comfy means hosted weights. A closed provider's engine, or fal, means API-only. A model-family engine such as wan or ltx2 settles nothing.
  • How existing versions of the same base model are set up.
  • For a new ecosystem, the @civitai/orchestration-client input type. Comfy* means hosted weights.

The deciding question is whether the provider publishes weights we run, or only an API. official-model-admin evidence collects these signals, and the user confirms the kind.

The upload wizard (/models/<modelId>/model-versions/<versionId>/wizard) skips its files step for ExternalGeneration. For hosted weights there are two routes: weights already on Hugging Face are queued at /moderator/huggingface-import and fetched server-side, then attached with official-model-admin attach-import; anything else is uploaded by hand at ?step=2, or through the Manage files item in the version menu. Either way a human drives it — this is the one step no skill completes on its own.

A hosted-weights version is ready to cover once it has a scanned file of a weight type, and a checkpoint also needs a SafeTensor file. That's the same rule checkLoadable in src/server/services/resource-load.service.ts applies. official-model-admin files checks it.

Launch

These steps are done by a human, after the deploy. The generator-launch skill (launch --version <id>) prints them with the real URLs and commands, and ticks off the steps already done.

  1. Publish, with the green Publish button in the version panel of /models/<modelId>?modelVersionId=<versionId>. On a Draft model this publishes the model and the version together; on a published model it publishes only the version.
  2. Remove the gate rule with onboard.mjs gate remove, or in the mod UI.

Publish before removing the gate. While the rule is in place, the published version stays hidden from non-mods, because a hidden rule also blocks site-wide canGenerate. Removing the gate first would show everyone a Draft model they can't generate with.

After that, only if relevant:

  • A GenerationBaseModel row makes community resources for downloadable weights generatable (coverage branch 3). Its key is the base model's display name.
  • An AuctionBase row adds a paid featured-resources auction. Whether to add one is a product decision. Its key is the ecosystem key.
  • Training support: the add-training-support skill.
  • An ecosystem landing page: the ecosystem-seo-page skill.

The EcosystemCheckpoints row overrides everything else: the version stays covered even after it is unpublished. An ExternalGeneration version is also covered by branch 2 once published, so deleting its row after launch is what lets a later unpublish remove coverage. So far, every row has been kept.

The SQL for all three coverage branches, GenerationBaseModel and AuctionBase is in the add-generation-support skill under "Post-onboarding".

Mistakes that fail silently

  • Wiring only one lane. The generator has two lanes, data-graph (src/shared/data-graph/generation/) and form-graph (src/shared/form-graph/generation/). Each has its own graph, handler and registration. The parity tests and handlers.differential.test.ts list ecosystems by hand, so a missing entry isn't flagged anywhere.
  • Skipping the cache bust. After the coverage insert, the generator reports the version as uncovered for up to an hour.
  • Misspelling the gate target. A rule with a wrong ecosystem key has no effect, and nothing distinguishes it from having no rule at all.