fix(krea2): edit workflow ran community checkpoints on turbo's control set

Removing modelLocked at the ecosystem level unlocked img2img:edit too, where
the variant is chosen per-version: any id that wasn't `raw` fell to editTurbo,
whose 15-step / cfg-2 ceilings sit below what an undistilled finetune needs.
The six finetunes #4602 names are all full finetunes, so on edit they were not
merely mis-defaulted but undriveable, with no way for the user to fix it.

Fall back to editRaw instead — symmetric with the txt2img fallback this PR
already established, behaviour-identical for both official bases, and it adds
no new silently-substituting (ecosystem, workflow) pair to the #3520 population.

Also:
- ask "is this official?" in one place (isOfficialKrea2Version) rather than
  denylisting raw/turbo in the handler, so a fifth build is one edit
- cover the txt2img path: diffusionModel absent on the official builds and
  present on community ones, plus the FAL size tiers and checkpoint+LoRA
- add krea2-graph.test.ts pinning the unlock itself. With modelLocked on, the
  checkpoint clamp in common.ts rewrote every non-official id before the
  handler ever ran, and no test observed it — the feature could be reverted
  green
- drop comments this change made false: the header claimed edit picks its
  variant regardless of version, and training.ts called the checkpoint locked

Coverage is still the gate: all 201 community Krea 2 checkpoint models are
covered:false today, so generation coverage needs flipping before the #4602
finetunes can actually run or enter auctions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
briant
2026-09-03 15:48:46 -06:00
parent c0ddb36434
commit 9d3bc3f420
6 changed files with 150 additions and 25 deletions
@@ -1123,7 +1123,7 @@ export const ecosystemSupport: EcosystemSupport[] = [
{ ecosystemId: ECO.Ernie, supportType: 'generation', modelTypes: checkpointAndLora },
{ ecosystemId: ECO.Ernie, supportType: 'training', modelTypes: loraOnly },
// Krea 2 - base/turbo comfy variants support LoRA and community checkpoints (medium/large FAL tiers do not); LoRA training via AI-Toolkit
// Krea 2 - raw/turbo comfy variants support LoRA and community checkpoints (medium/large FAL tiers do not); LoRA training via AI-Toolkit
{
ecosystemId: ECO.Krea2,
supportType: 'generation',
@@ -4,11 +4,14 @@ import { createKrea2Input } from '../ecosystems/krea2.handler';
import { krea2VersionIds } from '~/shared/data-graph/generation/krea2-graph';
import type { GenerationHandlerCtx } from '../orchestration-new.service';
const communityVersionId = 8888;
const airsByVersionId: Record<number, string> = {
[krea2VersionIds.raw]: 'urn:air:krea2:checkpoint:civitai:2656567@3072329',
[krea2VersionIds.turbo]: 'urn:air:krea2:checkpoint:civitai:2656567@3072332',
[krea2VersionIds.large]: 'urn:air:krea2:checkpoint:civitai:2656567@2983022',
9001: 'urn:air:krea2:lora:civitai:9000@9001',
[communityVersionId]: 'urn:air:krea2:checkpoint:civitai:8000@8888',
};
const ctx = {
@@ -37,6 +40,54 @@ const editData = {
images: [{ url: 'https://example.com/a.png' }],
};
const createData = {
ecosystem: 'Krea2',
workflow: 'txt2img',
prompt: 'a cat in a hat',
quantity: 1,
aspectRatio: { value: '1:1', width: 1024, height: 1024 },
};
describe('createKrea2Input — txt2img', () => {
it.each([
['raw', krea2VersionIds.raw, 'raw'],
['turbo', krea2VersionIds.turbo, 'turbo'],
])('runs the official %s build on its own weights', async (_label, id, model) => {
const result = await input({ ...createData, model: { id } });
expect(result).toMatchObject({
engine: 'comfy',
ecosystem: 'krea2',
model,
operation: 'createImage',
});
expect(result.diffusionModel).toBeUndefined();
});
it('runs a community checkpoint on the raw build via diffusionModel', async () => {
const result = await input({
...createData,
model: { id: communityVersionId },
resources: [{ id: 9001, strength: 0.8 }],
});
expect(result).toMatchObject({
engine: 'comfy',
ecosystem: 'krea2',
model: 'raw',
operation: 'createImage',
diffusionModel: airsByVersionId[communityVersionId],
loras: { [airsByVersionId[9001]]: 0.8 },
});
});
it('keeps the FAL size tiers off the comfy path', async () => {
const result = await input({ ...createData, model: { id: krea2VersionIds.large } });
expect(result).toMatchObject({ engine: 'fal', model: 'krea2', size: 'large' });
});
});
describe('createKrea2Input — img2img:edit', () => {
it.each([
['turbo', krea2VersionIds.turbo],
@@ -1,8 +1,7 @@
/**
* Krea 2 Ecosystem Handler
*
* The Krea 2 checkpoint is locked, but its version selector splits across two
* engines (see krea2-graph.ts):
* The Krea 2 version selector splits across two engines (see krea2-graph.ts):
*
* - medium/large → FAL engine (Krea2FalImageGenInput). Size tiers, no LoRA;
* exposes creativity + style references.
@@ -27,7 +26,11 @@ import type {
import { removeEmpty } from '~/utils/object-helpers';
import type { GenerationGraphTypes } from '~/shared/data-graph/generation/generation-graph';
import type { ResourceData } from '~/shared/data-graph/generation/common';
import { krea2VersionIds, krea2VersionIdToSize } from '~/shared/data-graph/generation/krea2-graph';
import {
isOfficialKrea2Version,
krea2VersionIds,
krea2VersionIdToSize,
} from '~/shared/data-graph/generation/krea2-graph';
import { defineHandler } from './handler-factory';
type EcosystemGraphOutput = Extract<GenerationGraphTypes['Ctx'], { ecosystem: string }>;
@@ -89,18 +92,14 @@ export const createKrea2Input = defineHandler<Krea2Ctx, [ImageGenStepTemplate]>(
const images = 'images' in data ? data.images?.map((x) => x.url) : undefined;
if (isEdit && !images?.length) throw new Error('At least one image is required to edit');
// `model: 'edit'` selects the edit graph, not a checkpoint — the base build it
// runs on rides along as a diffusionModel AIR (same override the LTX handler
// uses to point at a community fine-tune).
// `diffusionModel` overrides the weights the named build loads (same override
// the LTX handler uses to point at a community fine-tune). Edit always needs it:
// `model: 'edit'` names the edit graph, not a checkpoint.
let diffusionModel: string | undefined;
if (isEdit) {
if (!data.model) throw new Error('A Krea 2 base model is required to edit');
diffusionModel = ctx.airs.getOrThrow(data.model.id);
} else if (
data.model &&
data.model.id !== krea2VersionIds.raw &&
data.model.id !== krea2VersionIds.turbo
) {
} else if (data.model && !isOfficialKrea2Version(data.model.id)) {
diffusionModel = ctx.airs.getOrThrow(data.model.id);
}
@@ -0,0 +1,77 @@
import { describe, expect, it } from 'vitest';
import { generationGraph } from './generation-graph';
import { isOfficialKrea2Version, krea2VersionIds } from './krea2-graph';
import type { GenerationCtx } from './context';
const ext: GenerationCtx = {
limits: { maxQuantity: 4, maxResources: 9, vidQuantity: 1 },
user: { isMember: true, tier: 'gold' },
gateRules: [],
};
const communityVersionId = 8888;
function init(workflow: string, modelId: number) {
const graph = generationGraph as any;
graph.init(
{
workflow,
ecosystem: 'Krea2',
model: { id: modelId, baseModel: 'Krea 2', model: { type: 'Checkpoint' } },
},
ext
);
return graph;
}
const selectedModelId = (g: any) => g.getSnapshot().model?.id;
describe('krea2 community checkpoints', () => {
// Pins the unlock: with `modelLocked` on, common.ts's checkpoint clamp rewrote
// every non-official id to the ecosystem default before the handler ever ran.
it('lets a non-official checkpoint through on txt2img', () => {
expect(selectedModelId(init('txt2img', communityVersionId))).toBe(communityVersionId);
});
it('gives it the comfy raw controls, not the FAL ones', () => {
const g = init('txt2img', communityVersionId);
expect(g.hasNode('cfgScale')).toBe(true);
expect(g.hasNode('steps')).toBe(true);
expect(g.hasNode('creativity')).toBe(false);
});
// An official build missing from krea2VersionIdToVariant reads as community, and
// the handler sends it to the orchestrator as an override on the raw build.
it.each(Object.entries(krea2VersionIds))('recognises official %s', (_name, id) => {
expect(isOfficialKrea2Version(id)).toBe(true);
});
it('still gives the official size tiers the FAL controls', () => {
const g = init('txt2img', krea2VersionIds.medium);
expect(g.hasNode('creativity')).toBe(true);
expect(g.hasNode('cfgScale')).toBe(false);
});
});
describe('krea2 img2img:edit bases', () => {
const cfgMax = (g: any) => g.getSnapshot('cfgScale').meta.max;
const stepsMax = (g: any) => g.getSnapshot('steps').meta.max;
it.each([
['turbo', krea2VersionIds.turbo, 2, 15],
['raw', krea2VersionIds.raw, 10, 60],
])('keeps the official %s base on its own control range', (_l, id, cfg, steps) => {
const g = init('img2img:edit', id);
expect(cfgMax(g)).toBe(cfg);
expect(stepsMax(g)).toBe(steps);
});
// Turbo's ceilings sit below what an undistilled finetune needs, so landing a
// community checkpoint there leaves the user no way to drive it.
it('gives a community checkpoint the full-step range, not turbo ceilings', () => {
const g = init('img2img:edit', communityVersionId);
expect(selectedModelId(g)).toBe(communityVersionId);
expect(cfgMax(g)).toBe(10);
expect(stepsMax(g)).toBe(60);
});
});
+10 -12
View File
@@ -16,10 +16,10 @@
* - turbo: distilled, low-step variant
* Controls: aspectRatio / resources (LoRA) / negativePrompt / cfgScale / steps / seed.
*
* The selected version is mapped to a `krea2Variant` discriminator
* ('fal' | 'raw' | 'turbo') that swaps in the engine-appropriate controls. The
* `img2img:edit` workflow selects a fourth 'edit' variant regardless of version
* — Krea 2 exposes editing as an operation, not as its own build.
* The selected version is mapped to a `krea2Variant` discriminator that swaps in
* the engine-appropriate controls. `img2img:edit` maps to editRaw/editTurbo —
* still per-version, because Krea 2 exposes editing as an operation on either
* build, not as its own build.
*/
import z from 'zod';
@@ -95,11 +95,6 @@ export const krea2VersionIdToSize = new Map<number, Krea2Size>([
[krea2VersionIds.large, 'large'],
]);
/**
* Map version ID → control-set variant. medium/large share the FAL control set;
* raw/turbo each get the comfy control set. Unknown IDs (community checkpoints)
* fall back to 'raw'.
*/
const krea2VersionIdToVariant = new Map<number, Krea2Variant>([
[krea2VersionIds.medium, 'fal'],
[krea2VersionIds.large, 'fal'],
@@ -107,6 +102,8 @@ const krea2VersionIdToVariant = new Map<number, Krea2Variant>([
[krea2VersionIds.turbo, 'turbo'],
]);
export const isOfficialKrea2Version = (id: number) => krea2VersionIdToVariant.has(id);
// =============================================================================
// Aspect Ratios
// =============================================================================
@@ -337,13 +334,14 @@ export const krea2Graph = new DataGraph<
priorityOptions: krea2PriorityRatios,
})
)
// Derive the control-set variant from the selected version, then swap in the
// engine-appropriate controls (FAL: creativity/styleRefs; comfy: LoRA/cfg/steps).
// Unknown ids are community checkpoints. Only the comfy builds can load one via
// `diffusionModel`, so they fall back off the FAL tiers — and to the full-step
// build, since turbo's 15-step / cfg-2 ceilings can't drive an undistilled model.
.computed(
'krea2Variant',
(ctx): Krea2Variant => {
if (ctx.workflow === 'img2img:edit')
return ctx.model?.id === krea2VersionIds.raw ? 'editRaw' : 'editTurbo';
return ctx.model?.id === krea2VersionIds.turbo ? 'editTurbo' : 'editRaw';
return (ctx.model?.id ? krea2VersionIdToVariant.get(ctx.model.id) : undefined) ?? 'raw';
},
['model', 'workflow']
+1 -1
View File
@@ -357,7 +357,7 @@ export const trainingModelInfo: {
description: "Krea AI's in-house image generation model.",
// Krea 2 is an AI-Toolkit-only ecosystem, so this AIR is NOT sent as the orchestrator
// `model` (the orchestrator resolves the base model from the ecosystem); it's only used for
// UI display / getModel. Points at the locked Krea 2 generation checkpoint.
// UI display / getModel. Points at a Krea 2 generation checkpoint.
air: 'urn:air:krea2:checkpoint:civitai:2656567@2983022',
baseModel: 'Krea 2',
isNew: true,