Add Training Support
Wires an existing ecosystem into the training form (the "Train a LoRA" flow), distinct from the generation form. After this, the ecosystem shows up as a selectable base model in Training Step 1 and submits a valid training step to the orchestrator.
Training is a separate subsystem from generation. The generation handlers/graphs in src/server/services/orchestrator/ecosystems/* and src/shared/data-graph/generation/* are NOT part of this — do not touch them. The files below are the training path.
When to use
- A new model family was just added via
add-ecosystem and now needs to be trainable
- Re-enabling training for an ecosystem that was commented out
- The orchestrator/
@civitai/client gained a new *TrainingInput type
Prerequisites
The ecosystem, base model, family and license must already exist in
basemodel.constants.ts (ECO.<Name>,
an EcosystemRecord, a BaseModelRecord). If any are missing, run add-ecosystem first.
Almost every ecosystem added recently is AI-Toolkit-based (engine 'ai-toolkit',
mandatory). The worked reference is anima — it is the simplest complete example
(image, AI-Toolkit-only, no modelVariant). The single highest-leverage move when adding
a new ecosystem: grep every file below for anima and add a parallel entry.
Step 0 — Check @civitai/client for the training input type
Always confirm the SDK ships the ecosystem's training type before wiring. It tells you
the exact field shape, whether modelVariant is required, and any fixed fields.
grep -niE "<Name>AiToolkitTrainingInput|ecosystem: '<name>'" \
node_modules/@civitai/client/dist/generated/types.gen.d.ts
Note from the type:
ecosystem literal — the exact string the orchestrator expects (e.g. 'boogu').
modelVariant — present (e.g. flux1 'dev'|'schnell', wan '2.1'|'2.2') or absent.
- Fixed/constrained fields — e.g. Boogu declares
batchSize?: null | number "Fixed at 1
for this ecosystem". Honor these in the UI param bounds.
readonly outputs (defaultSteps, storageBuzzPerEpoch, maxBatchSize, …) — server-
computed; never send them.
If the type is missing in the installed version, check the latest (npm view @civitai/client versions --json | tail) and bump with pnpm add @civitai/client@<v>. If still absent, the
orchestrator dispatch (training.orch.ts) already casts ecosystem as any, so it works — but
prefer a typed SDK.
Step 1 — Gather the ecosystem's training defaults
Get these from the user, the model card, or an orchestrator whatif sample request:
- Default steps (the AI-Toolkit primary length knob — drives pricing)
- lr (unet LR), whether the text encoder is trained (usually disabled)
- networkDim / networkAlpha, lrScheduler, optimizerType, noiseOffset
- batch size bounds (many AI-Toolkit image ecosystems are fixed at 1)
- resolution (image ecosystems are typically 1024)
- The base-model AIR. If the model isn't on civitai yet, use the HF repository URN from the
sample as a placeholder and leave a comment to swap in
urn:air:<eco>:checkpoint:civitai:<modelId>@<versionId> once uploaded. For AI-Toolkit-only
ecosystems this AIR is NOT sent to the orchestrator (it resolves the base model from the
ecosystem); it's used for UI display / getTrainingFields.getModel.
Pick a stable base-model key (the trainingModelInfo key, e.g. boogu) and a
baseType (the TrainingBaseModelType, e.g. 'boogu'). They can differ (SD has
sd_1_5/anime/… keys all mapping to baseType sd15), but for a single-checkpoint
ecosystem keep them the same.
Step 2 — Confirm the plan with the user
Adding training support for: <Name> (baseType: <baseType>, key: <key>)
Engine: ai-toolkit (mandatory) Variant: <none | enum>
Defaults: steps <n>, lr <n>, dim/alpha <n>/<n>, scheduler <x>, batch <n> (max <n>), res <n>
AIR: <placeholder|civitai urn> Feature flag: <name>Training / <name>-training (Flipt)
Wait for confirmation, then make all edits in one pass.
Step 3 — Edits
3a. src/utils/training.ts (the core — ~7 spots)
trainingBaseModelTypesImage (or …Video / …Audio) — add '<baseType>'.
aiToolkitStepDefault — add a branch returning the ecosystem's default steps if it
differs from 2000.
aiToolkitBatchMax — add a branch only if max batch > 1 (default returns 1).
trainingModelInfo — add the <key>: { label, pretty, type: '<baseType>', description, air, baseModel: '<BaseModelName>', isNew: true, aiToolkit: { ecosystem: '<eco>' [, modelVariant] } } entry. baseModel MUST match the BaseModelRecord.name /
ecosystem key in basemodel.constants.ts exactly (a mismatch produces a malformed AIR on
training completion and fails the post-train scan with 400 — see the inline comment on the
hidream_o1 entry).
baseTypeToEcosystem — add <baseType>: '<eco>'.
isAiToolkitSupported supportedTypes — add '<baseType>'.
isAiToolkitMandatory mandatoryTypes — add '<baseType>' (for AI-Toolkit-only
ecosystems). This auto-enables sample-prompt requirements and AI-Toolkit gating.
getDefaultEngine — add if (baseType === '<baseType>') return 'ai-toolkit';.
3b. src/server/schema/model-version.schema.ts
- Add
export const trainingDetailsBaseModels<Name> = ['<key>'] as const; next to the others.
- Spread
...trainingDetailsBaseModels<Name> into the matching aggregate
(trainingDetailsBaseModelsImage / …Video / …Audio).
- The
trainingDetailsObj zod enums (baseModel, baseModelType) derive from these +
trainingBaseModelType, so they update automatically. No baseModelToTraningDetailsBaseModelMap
entry unless mapping a BaseModel display name back to a key (Wan does this).
3c. src/server/schema/orchestrator/training.schema.ts
3d. src/server/services/feature-flags.service.ts
- Add
<name>Training: { availability: ['mod'], fliptKey: '<name>-training' },. Create the
Flipt flag too (use the flipt skill). Mod-only is the norm for a new/experimental base model.
3e. src/components/Training/Form/TrainingParams.tsx
- The
trainingSettings array drives the UI and per-base defaults. Each setting's overrides
map is keyed by base-model key (<key>), not baseType. Grep the file for the reference
ecosystem's key (anima:) and add a parallel <key>: { all: { … } } entry to each setting
where the new ecosystem should differ from the base default. For an AI-Toolkit image ecosystem
that's typically: engine ('ai-toolkit'), maxTrainEpochs, trainBatchSize (honor the
fixed/max from the SDK type), targetSteps, saveEvery, resolution, shuffleCaption
(disabled), keepTokens (disabled), unetLR, textEncoderLR (disabled), lrScheduler,
minSnrGamma (disabled), networkDim, networkAlpha, noiseOffset, optimizerArgs.
Skip a setting if the base default already matches (e.g. optimizerType AdamW8Bit,
flipAugmentation false for image).
3f. src/components/Training/Form/TrainingSubmitModelSelect.tsx
- Import
trainingDetailsBaseModels<Name>.
- Add
const baseModel<Name> = !!formBaseModel && (trainingDetailsBaseModels<Name> as ReadonlyArray<string>).includes(formBaseModel) ? formBaseModel : null;.
- Add a
{features.<name>Training && (<ModelSelector … name="<Name>" value={baseModel<Name>} baseType="<baseType>" makeDefaultParams={makeDefaultParams} isNew={…} />)} block under the
right mediaType group (image/video/audio). Pick an unused color.
- Add
selectedRun.baseType === '<baseType>' || to the "experimental build" alert condition
(for new/experimental base models).
3g. src/shared/constants/basemodel.constants.ts
Files that need NO change (generic / arbitrary-ecosystem aware)
src/server/services/orchestrator/training/training.orch.ts — createTrainingStep_AiToolkit
builds the input generically and casts ecosystem as any. Only add a branch if the ecosystem
needs special fields (sd1/sdxl send model + minSnrGamma; ACE-Step sends samplesOverrides).
src/store/training.store.ts — getDefaultTrainingParams reads trainingSettings overrides
by key. Only touch if the new ecosystem should become a form default.
src/components/Training/Form/TrainingSubmit.tsx / TrainingSubmitAdvancedSettings.tsx —
generic; resolve the ecosystem via getAiToolkitEcosystem once 3a is done.
src/server/common/enums.ts OrchEngineTypes — ai-toolkit already present.
Step 4 — Typecheck
pnpm run typecheck
Common failures: a baseType/key typo (the literal won't match the TrainingBaseModelType
union), a missing spread in the aggregate array, or a baseModel string that isn't a valid
BaseModel. Iterate until clean.
Step 5 — Verify (optional)
With a dev server running (see dev-server skill) and the Flipt flag on for your user:
- Open the training form → Step 1 shows the new base model under its media type.
- Selecting it loads the expected default params in Step 3.
- The
whatif submit returns a price without a validation error.
Notes
- Mandatory vs optional AI-Toolkit: mandatory ecosystems (the common case) are gated solely
by their own
<name>Training flag. Optional ones (sd15/sdxl/flux/…) additionally check a
per-model aiToolkit<X> flag via aiToolkitFlagByBaseType — only relevant if you're adding an
ecosystem that also supports Kohya.
- Audio ecosystems additionally gate on
audioTraining and may send samplesOverrides
(see ACE-Step). Video ecosystems go in trainingBaseModelTypesVideo and usually disable
spatial params (resolution/clipSkip/noiseOffset).
- Batch size: if the SDK type fixes it (e.g. Boogu = 1), set the
trainBatchSize override to
{ all: { default: 1, min: 1, max: 1 } } and leave aiToolkitBatchMax at its default.
- Placeholder AIR: fine to ship before the base model is on civitai — it's not sent for
AI-Toolkit-only ecosystems. Leave a comment so it gets swapped to the civitai URN later.
1---2name: add-training-support3description: Wire an existing ecosystem into the LoRA training system so it appears as a trainable base model in the training form. Adds the base-model entry, schema enums, orchestrator validation, feature flag, and per-ecosystem default params. Use when a model family (already in basemodel.constants.ts) needs training support — e.g. AI-Toolkit ecosystems like Anima, HiDream, Boogu. Always checks @civitai/client for the ecosystem's training input type first.4---5
6# Add Training Support
7
8Wires an existing ecosystem into the **training** form (the "Train a LoRA" flow), distinct from the generation form. After this, the ecosystem shows up as a selectable base model in Training Step 1 and submits a valid `training` step to the orchestrator.
9
10Training is a **separate subsystem from generation**. The generation handlers/graphs in `src/server/services/orchestrator/ecosystems/*` and `src/shared/data-graph/generation/*` are NOT part of this — do not touch them. The files below are the training path.
11
12## When to use
13
14- A new model family was just added via `add-ecosystem` and now needs to be trainable
15- Re-enabling training for an ecosystem that was commented out
16- The orchestrator/`@civitai/client` gained a new `*TrainingInput` type
17
18## Prerequisites
19
20The ecosystem, base model, family and license must already exist in
21[basemodel.constants.ts](src/shared/constants/basemodel.constants.ts) (`ECO.<Name>`,
22an `EcosystemRecord`, a `BaseModelRecord`). If any are missing, run **add-ecosystem** first.
23
24Almost every ecosystem added recently is **AI-Toolkit-based** (engine `'ai-toolkit'`,
25mandatory). The worked reference is **`anima`** — it is the simplest complete example
26(image, AI-Toolkit-only, no `modelVariant`). The single highest-leverage move when adding
27a new ecosystem: **grep every file below for `anima` and add a parallel entry.**
28
29## Step 0 — Check `@civitai/client` for the training input type
30
31**Always** confirm the SDK ships the ecosystem's training type before wiring. It tells you
32the exact field shape, whether `modelVariant` is required, and any fixed fields.
33
34```bash
35grep -niE "<Name>AiToolkitTrainingInput|ecosystem: '<name>'" \
36 node_modules/@civitai/client/dist/generated/types.gen.d.ts
37```
38
39Note from the type:
40- **`ecosystem` literal** — the exact string the orchestrator expects (e.g. `'boogu'`).
41- **`modelVariant`** — present (e.g. flux1 `'dev'|'schnell'`, wan `'2.1'|'2.2'`) or absent.
42- **Fixed/constrained fields** — e.g. Boogu declares `batchSize?: null | number` "Fixed at 1
43 for this ecosystem". Honor these in the UI param bounds.
44- **`readonly` outputs** (`defaultSteps`, `storageBuzzPerEpoch`, `maxBatchSize`, …) — server-
45 computed; never send them.
46
47If the type is missing in the installed version, check the latest (`npm view @civitai/client
48versions --json | tail`) and bump with `pnpm add @civitai/client@<v>`. If still absent, the
49orchestrator dispatch (`training.orch.ts`) already casts `ecosystem as any`, so it works — but
50prefer a typed SDK.
51
52## Step 1 — Gather the ecosystem's training defaults
53
54Get these from the user, the model card, or an orchestrator `whatif` sample request:
55
56- **Default steps** (the AI-Toolkit primary length knob — drives pricing)
57- **lr** (unet LR), whether the **text encoder** is trained (usually disabled)
58- **networkDim / networkAlpha**, **lrScheduler**, **optimizerType**, **noiseOffset**
59- **batch size** bounds (many AI-Toolkit image ecosystems are fixed at 1)
60- **resolution** (image ecosystems are typically 1024)
61- The **base-model AIR**. If the model isn't on civitai yet, use the HF repository URN from the
62 sample as a placeholder and leave a comment to swap in
63 `urn:air:<eco>:checkpoint:civitai:<modelId>@<versionId>` once uploaded. For AI-Toolkit-only
64 ecosystems this AIR is NOT sent to the orchestrator (it resolves the base model from the
65 ecosystem); it's used for UI display / `getTrainingFields.getModel`.
66
67Pick a stable **base-model key** (the `trainingModelInfo` key, e.g. `boogu`) and a
68**baseType** (the `TrainingBaseModelType`, e.g. `'boogu'`). They can differ (SD has
69`sd_1_5`/`anime`/… keys all mapping to baseType `sd15`), but for a single-checkpoint
70ecosystem keep them the same.
71
72## Step 2 — Confirm the plan with the user
73
74```
75Adding training support for: <Name> (baseType: <baseType>, key: <key>)
76Engine: ai-toolkit (mandatory) Variant: <none | enum>
77Defaults: steps <n>, lr <n>, dim/alpha <n>/<n>, scheduler <x>, batch <n> (max <n>), res <n>
78AIR: <placeholder|civitai urn> Feature flag: <name>Training / <name>-training (Flipt)
79```
80
81Wait for confirmation, then make all edits in one pass.
82
83## Step 3 — Edits
84
85### 3a. `src/utils/training.ts` (the core — ~7 spots)
86
871. **`trainingBaseModelTypesImage`** (or `…Video` / `…Audio`) — add `'<baseType>'`.
882. **`aiToolkitStepDefault`** — add a branch returning the ecosystem's default steps if it
89 differs from 2000.
903. **`aiToolkitBatchMax`** — add a branch only if max batch > 1 (default returns 1).
914. **`trainingModelInfo`** — add the `<key>: { label, pretty, type: '<baseType>', description,
92 air, baseModel: '<BaseModelName>', isNew: true, aiToolkit: { ecosystem: '<eco>'
93 [, modelVariant] } }` entry. `baseModel` MUST match the `BaseModelRecord.name` /
94 ecosystem `key` in basemodel.constants.ts exactly (a mismatch produces a malformed AIR on
95 training completion and fails the post-train scan with 400 — see the inline comment on the
96 `hidream_o1` entry).
975. **`baseTypeToEcosystem`** — add `<baseType>: '<eco>'`.
986. **`isAiToolkitSupported`** `supportedTypes` — add `'<baseType>'`.
997. **`isAiToolkitMandatory`** `mandatoryTypes` — add `'<baseType>'` (for AI-Toolkit-only
100 ecosystems). This auto-enables sample-prompt requirements and AI-Toolkit gating.
1018. **`getDefaultEngine`** — add `if (baseType === '<baseType>') return 'ai-toolkit';`.
102
103### 3b. `src/server/schema/model-version.schema.ts`
104
105- Add `export const trainingDetailsBaseModels<Name> = ['<key>'] as const;` next to the others.
106- Spread `...trainingDetailsBaseModels<Name>` into the matching aggregate
107 (`trainingDetailsBaseModelsImage` / `…Video` / `…Audio`).
108- The `trainingDetailsObj` zod enums (`baseModel`, `baseModelType`) derive from these +
109 `trainingBaseModelType`, so they update automatically. No `baseModelToTraningDetailsBaseModelMap`
110 entry unless mapping a `BaseModel` display name back to a key (Wan does this).
111
112### 3c. `src/server/schema/orchestrator/training.schema.ts`
113
114- Add a branch to the `aiToolkitTrainingParams` discriminated union:
115 ```ts
116 aiToolkitBaseParams.extend({ ecosystem: z.literal('<eco>'), modelVariant: z.undefined().optional() }),
117 ```
118 (or `modelVariant: z.enum([...])` if the SDK type requires one). Without this, submission
119 validation rejects the new ecosystem.
120
121### 3d. `src/server/services/feature-flags.service.ts`
122
123- Add `<name>Training: { availability: ['mod'], fliptKey: '<name>-training' },`. Create the
124 Flipt flag too (use the `flipt` skill). Mod-only is the norm for a new/experimental base model.
125
126### 3e. `src/components/Training/Form/TrainingParams.tsx`
127
128- The `trainingSettings` array drives the UI and per-base defaults. Each setting's `overrides`
129 map is keyed by **base-model key** (`<key>`), not baseType. Grep the file for the reference
130 ecosystem's key (`anima:`) and add a parallel `<key>: { all: { … } }` entry to **each** setting
131 where the new ecosystem should differ from the base default. For an AI-Toolkit image ecosystem
132 that's typically: `engine` (`'ai-toolkit'`), `maxTrainEpochs`, `trainBatchSize` (honor the
133 fixed/max from the SDK type), `targetSteps`, `saveEvery`, `resolution`, `shuffleCaption`
134 (disabled), `keepTokens` (disabled), `unetLR`, `textEncoderLR` (disabled), `lrScheduler`,
135 `minSnrGamma` (disabled), `networkDim`, `networkAlpha`, `noiseOffset`, `optimizerArgs`.
136 Skip a setting if the base default already matches (e.g. `optimizerType` AdamW8Bit,
137 `flipAugmentation` false for image).
138
139### 3f. `src/components/Training/Form/TrainingSubmitModelSelect.tsx`
140
141- Import `trainingDetailsBaseModels<Name>`.
142- Add `const baseModel<Name> = !!formBaseModel && (trainingDetailsBaseModels<Name> as
143 ReadonlyArray<string>).includes(formBaseModel) ? formBaseModel : null;`.
144- Add a `{features.<name>Training && (<ModelSelector … name="<Name>" value={baseModel<Name>}
145 baseType="<baseType>" makeDefaultParams={makeDefaultParams} isNew={…} />)}` block under the
146 right `mediaType` group (image/video/audio). Pick an unused `color`.
147- Add `selectedRun.baseType === '<baseType>' ||` to the "experimental build" alert condition
148 (for new/experimental base models).
149
150### 3g. `src/shared/constants/basemodel.constants.ts`
151
152- Add the training support entry to `ecosystemSupport`:
153 ```ts
154 { ecosystemId: ECO.<Name>, supportType: 'training', modelTypes: loraOnly },
155 ```
156 (`loraOnly` is the standard for trained outputs — they're LoRAs.) Generation support is
157 independent; don't add it unless the ecosystem is also generatable.
158
159### Files that need NO change (generic / arbitrary-ecosystem aware)
160
161- `src/server/services/orchestrator/training/training.orch.ts` — `createTrainingStep_AiToolkit`
162 builds the input generically and casts `ecosystem as any`. Only add a branch if the ecosystem
163 needs special fields (sd1/sdxl send `model` + `minSnrGamma`; ACE-Step sends `samplesOverrides`).
164- `src/store/training.store.ts` — `getDefaultTrainingParams` reads `trainingSettings` overrides
165 by key. Only touch if the new ecosystem should become a form default.
166- `src/components/Training/Form/TrainingSubmit.tsx` / `TrainingSubmitAdvancedSettings.tsx` —
167 generic; resolve the ecosystem via `getAiToolkitEcosystem` once 3a is done.
168- `src/server/common/enums.ts` `OrchEngineTypes` — `ai-toolkit` already present.
169
170## Step 4 — Typecheck
171
172```bash
173pnpm run typecheck
174```
175
176Common failures: a `baseType`/key typo (the literal won't match the `TrainingBaseModelType`
177union), a missing spread in the aggregate array, or a `baseModel` string that isn't a valid
178`BaseModel`. Iterate until clean.
179
180## Step 5 — Verify (optional)
181
182With a dev server running (see `dev-server` skill) and the Flipt flag on for your user:
183- Open the training form → Step 1 shows the new base model under its media type.
184- Selecting it loads the expected default params in Step 3.
185- The `whatif` submit returns a price without a validation error.
186
187## Notes
188
189- **Mandatory vs optional AI-Toolkit**: mandatory ecosystems (the common case) are gated solely
190 by their own `<name>Training` flag. Optional ones (sd15/sdxl/flux/…) additionally check a
191 per-model `aiToolkit<X>` flag via `aiToolkitFlagByBaseType` — only relevant if you're adding an
192 ecosystem that also supports Kohya.
193- **Audio ecosystems** additionally gate on `audioTraining` and may send `samplesOverrides`
194 (see ACE-Step). **Video ecosystems** go in `trainingBaseModelTypesVideo` and usually disable
195 spatial params (resolution/clipSkip/noiseOffset).
196- **Batch size**: if the SDK type fixes it (e.g. Boogu = 1), set the `trainBatchSize` override to
197 `{ all: { default: 1, min: 1, max: 1 } }` and leave `aiToolkitBatchMax` at its default.
198- **Placeholder AIR**: fine to ship before the base model is on civitai — it's not sent for
199 AI-Toolkit-only ecosystems. Leave a comment so it gets swapped to the civitai URN later.