* feat(contests): gate model entries on allowed base models Contest collections can now declare `metadata.baseModels`; a model entry only qualifies if it has a live version on one of them. Empty/absent means no gating, so existing contests are unaffected. The date rule and the base-model rule are satisfied by a SINGLE version predicate rather than two independent checks — otherwise a two-year-old SDXL LoRA could qualify by pairing an old allowed-base-model version with a throwaway version pushed during the submission window. The model block also moved out of `if (metadata.submissionStartDate)` so base-model gating still applies to windowless contests. Moderators are not exempt, matching the neighbouring date/window checks (the eligibility family) rather than `maxItemsPerUser` (an anti-spam quota mods legitimately bypass when curating). A mod who wants an exception edits the contest's allowed base models instead of side-stepping the rule. * fix(contests): harden base-model gating and lock the where shape Review follow-ups on the base-model gate: - Guard the model-row date filter on submissionStartDate being set. It was unreachable with undefined today, but Prisma's lt is optional so undefined type-checks, and loosening the outer guard later would silently drop the filter and let every model through. - Validate metadata.baseModels against the known base models. The value has to match ModelVersion.baseModel exactly; an unrecognized one like 'FluxKrea' matched no version and locked the contest to zero entries while reporting a plausible-looking error. - Spell out in the field description that with a submission window the same version must satisfy both rules. Adds tests covering the where shape, most importantly that the no-gating path is unchanged -- a live contest with accepted entries depends on it -- and that the date and base-model conditions stay in one modelVersions.none object. Draft versions still qualify. Requiring Published would re-block the 69 in-window draft Krea 2 versions the preceding commits deliberately unblocked, and entries pass through moderator review anyway. * fix(contests): source base models from basemodel.constants The gate read the legacy base-model.constants.ts, whose list stops short of current ecosystems -- 'Krea 2' is absent from it, so the very contest this was built for could not be configured, and the zod refine would have rejected the value outright. basemodel.constants.ts is the live source (95 importers vs 4, and it carries a compatibility layer explicitly marked as the migration target for the old file). Points the schema and the picker at it, moves the two remaining importers over, and deletes the legacy file. The picker now offers every base model rather than the non-hidden ones. Hidden means "not offered on upload", which is a different question from whether a contest can target it -- SD 3.5, SDXL Turbo, Kling and Sora 2 are all populated and were unselectable. The field is moderator-only. * docs: drop references to the removed base-model.constants Both docs described the legacy module as current. The plan doc keeps its original snippets with a note, since rewriting a historical plan would misrepresent what was proposed.
19 KiB
Base Model Constants Management
Overview
This document outlines a plan to migrate base model constants from hardcoded TypeScript files to database-managed records, enabling runtime configuration via an admin interface without requiring deployments.
Current State
Completed: New Constants File
A new constants file has been created at src/shared/constants/basemodel.constants.ts with a database-like structure using numeric IDs. This file consolidates all base model configuration and will serve as the seed data for database migration.
Records Created:
- Licenses: 24 records (IDs 1-24)
- Families: 12 records (IDs 1-12)
- Groups: 49 records (IDs 1-49)
- Base Models: 66 records (IDs 1-66)
- Generation Support: ~300+ records with auto-incrementing IDs
Exports:
licenses,licenseById- License definitionsbaseModelFamilies,familyById- Family groupingsbaseModelGroups,groupById,groupByKey- Group configurations with settingsbaseModels,baseModelById,baseModelByName- Base model definitionsgenerationSupport,generationSupportById- Generation compatibility matrix
Helper Functions:
getBaseModelsByGroupId(groupId: number)getGenerationSupportForGroup(groupId: number, modelType?: ModelType)getCompatibleBaseModels(groupId: number, modelType: ModelType)getBaseModelLicense(baseModelId: number)getGroupFamily(groupId: number)getGroupsByFamilyId(familyId: number)getGenerationBaseModels()- Base models available for generationgetAuctionBaseModels()- Base models available for auctiongetDeprecatedBaseModels()- Deprecated base models
Legacy Files (To Be Deprecated)
src/shared/constants/base-model.constants.ts has been removed. Its list had drifted from
reality — it never gained Krea 2 — so anything reading it saw a stale set of base models.
basemodel.constants.ts is the only source now.
src/server/common/constants.tsbaseLicenses- License definitions with URLs, names, notices, and NSFW restrictionsbaseModelLicenses- Mapping of base models to their licensesgenerationConfig- Generation settings per base model group (aspect ratios, default checkpoints)
Data Model
Entity Relationships
BaseModelFamily (e.g., "Flux", "StableDiffusion")
│
└── BaseModelGroup (e.g., "Flux1", "SDXL")
│
├── BaseModel (e.g., "Flux.1 D", "SDXL 1.0")
│
└── BaseModelGenerationSupport (matrix of modelType × baseModel × supportLevel)
Key Points:
- Base models belong to a group (required)
- Groups optionally belong to a family
- Base models do not have a direct family relationship
- Base model names must be unique and serve as the unique identifier (no separate key field)
- Groups have a
keyfield for programmatic access, while base models usenameas both display name and unique identifier
Database Schema
model License {
id Int @id @default(autoincrement())
name String // "Apache 2.0", "CreativeML Open RAIL-M"
url String?
notice String? // Required attribution text
poweredBy String? // "Powered by X" text
disableMature Boolean @default(false) // Restrict NSFW content
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
baseModels BaseModel[]
}
model BaseModelFamily {
id Int @id @default(autoincrement())
name String // Display name: "Flux", "Stable Diffusion"
description String?
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
groups BaseModelGroup[]
baseModels BaseModel[]
}
model BaseModelGroup {
id Int @id @default(autoincrement())
key String @unique // "SD1", "SDXL", "Flux1"
name String // Display name: "Stable Diffusion 1.x"
description String?
sortOrder Int @default(0)
// Generation settings
settings Json? // { aspectRatios: [{ label, width, height }], ... }
modelVersionId Int? // ModelVersion ID for default generation
familyId Int?
family BaseModelFamily? @relation(fields: [familyId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
baseModels BaseModel[]
generationSupport BaseModelGenerationSupport[]
}
model BaseModel {
id Int @id @default(autoincrement())
name String @unique // Display name (e.g., "SD 1.5", "SDXL 1.0", "Flux.1 D")
type MediaType // image, video
// Visibility & status flags (optional, only set when true)
hidden Boolean @default(false) // Hide from dropdowns
deprecated Boolean @default(false) // Mark as deprecated
// Capability flags (optional, only set when true)
canGenerate Boolean @default(false) // Available for generation
canTrain Boolean @default(false) // Available for training
canAuction Boolean @default(false) // Available for auction
// Technical metadata
ecosystem String? // "sdxl", "flux", "auraflow" - for resource filtering
engine String? // "wan", "hunyuan", "lightricks" - for orchestration
groupId Int
group BaseModelGroup @relation(fields: [groupId], references: [id])
licenseId Int?
license License? @relation(fields: [licenseId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
generationSupport BaseModelGenerationSupport[]
}
// Generation compatibility matrix
// Each record represents one cell: (group, modelType, baseModel) → supportLevel
model BaseModelGenerationSupport {
id Int @id @default(autoincrement())
groupId Int
group BaseModelGroup @relation(fields: [groupId], references: [id])
modelType ModelType // Checkpoint, LORA, TextualInversion, DoRA, LoCon, VAE
baseModelId Int
baseModel BaseModel @relation(fields: [baseModelId], references: [id])
support SupportLevel // full, partial
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([groupId, modelType, baseModelId])
}
enum SupportLevel {
full
partial
}
TypeScript Types (Current Implementation)
The constants file uses these corresponding TypeScript types:
type LicenseRecord = {
id: number;
name: string;
url?: string;
notice?: string;
poweredBy?: string;
disableMature?: boolean;
};
type BaseModelFamilyRecord = {
id: number;
name: string;
description: string;
sortOrder: number;
};
type BaseModelGroupRecord = {
id: number;
key: string;
name: string;
description: string;
familyId?: number;
sortOrder: number;
settings?: Record<string, any>;
modelVersionId?: number;
};
type BaseModelRecord = {
id: number;
name: string; // Must be unique - serves as both display name and unique identifier
type: MediaType;
hidden?: boolean;
deprecated?: boolean;
canGenerate?: boolean;
canTrain?: boolean;
canAuction?: boolean;
ecosystem?: string;
engine?: string;
groupId: number;
licenseId?: number;
};
type BaseModelGenerationSupportRecord = {
id: number;
groupId: number;
modelType: ModelType;
baseModelId: number;
support: SupportLevel;
};
Generation Support Matrix
The BaseModelGenerationSupport table creates a matrix for each group:
Example: SD1 Generation Config
| Model Type | SD 1.4 | SD 1.5 | SD 1.5 LCM | SD 1.5 Hyper |
|---|---|---|---|---|
| Checkpoint | full | full | full | full |
| TextualInversion | full | full | full | full |
| LORA | full | full | full | full |
| DoRA | full | full | full | full |
| LoCon | full | full | full | full |
| VAE | full | full | full | full |
Example: SDXL Generation Config (with cross-group partial support)
| Model Type | SDXL 0.9 | SDXL 1.0 | SDXL 1.0 LCM | SDXL Lightning | SD 1.5 | Pony |
|---|---|---|---|---|---|---|
| Checkpoint | full | full | full | full | - | - |
| TextualInversion | full | full | full | full | partial | partial |
| LORA | full | full | full | full | - | partial |
| VAE | full | full | full | full | - | partial |
Database records for this matrix (using numeric IDs):
| id | groupId | modelType | baseModelId | support |
|-----|---------|------------------|-------------|---------|
| 25 | 9 | Checkpoint | 44 | full | // SDXL group, SDXL 0.9
| 26 | 9 | Checkpoint | 45 | full | // SDXL group, SDXL 1.0
| 31 | 9 | TextualInversion | 45 | full | // SDXL group, SDXL 1.0
| 67 | 9 | TextualInversion | 31 | partial | // SDXL group, SD 1.5
| 37 | 9 | LORA | 45 | full | // SDXL group, SDXL 1.0
| 71 | 9 | LORA | 26 | partial | // SDXL group, Pony
| ... | ... | ... | ... | ... |
ID Reference:
- Group 9 = SDXL
- Base Model 44 = SDXL 0.9, 45 = SDXL 1.0, 31 = SD 1.5, 26 = Pony
Client-Side Data Access
Recommended Approach: Script Injection + Background Refresh
- Server caches config in Redis/memory (1-5 min TTL)
- SSR injects config into page via script tag
- Client reads from
window.__BASE_MODEL_CONFIG__synchronously - Background refresh for long-lived sessions
// Server: inject into HTML during SSR
function getConfigScript() {
const config = getCachedBaseModelConfig(); // From Redis/memory
return `<script>window.__BASE_MODEL_CONFIG__=${JSON.stringify(config)};</script>`;
}
// Shared access (works server + client)
export function getBaseModelConfig(): BaseModelConfigData {
if (typeof window !== 'undefined' && window.__BASE_MODEL_CONFIG__) {
return window.__BASE_MODEL_CONFIG__;
}
return getServerConfig(); // Server-side fallback
}
Benefits
- Synchronous access - No loading states, works in any context
- Type-safe - TypeScript types from Zod schema
- Fresh data - Background refresh for long sessions
- Resilient - Falls back gracefully if fetch fails
Admin UI
Base Model Management Checklist
When adding a new base model, the admin UI guides through these steps:
□ 1. Create/Select Base Model Family
- Key, name, description
□ 2. Create/Select Base Model Group
- Key, name, description
- Assign aspect ratios
- Set feature flags (generation, auction)
□ 3. Create Base Model Entry
- Name (e.g., "Flux.1 D") - used as unique identifier
- Media type (image/video)
- Assign to group (family is inherited from group)
- Set ecosystem, engine (if applicable)
□ 4. Configure License
- Select existing or create new license
- License URL, name, notice
- NSFW restrictions
□ 5. Configure Generation Support Matrix
- For each model type, set support level (full/partial/none)
- Configure cross-group partial support
□ 6. Link Default Checkpoint
- Select model version for default generation
- Verify model is published and accessible
□ 7. Verify & Publish
- Preview how it appears in UI
- Test generation with new base model
- Enable (unhide) when ready
Admin UI: Matrix Editor
For generation support, a matrix editor UI:
┌─────────────────────────────────────────────────────────────────────┐
│ SDXL Generation Support [Save] │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Group Members: SDXL 0.9, SDXL 1.0, SDXL 1.0 LCM, Lightning, Hyper │
│ Cross-Group: SD 1.5, Pony, Illustrious, NoobAI │
│ │
│ ┌──────────────┬────────┬────────┬──────────┬─────────┬──────────┐ │
│ │ │ SDXL │ SDXL │ SD 1.5 │ Pony │ Illust. │ │
│ │ │ 1.0 │ Light │ │ │ │ │
│ ├──────────────┼────────┼────────┼──────────┼─────────┼──────────┤ │
│ │ Checkpoint │ ● Full │ ● Full │ ○ None │ ○ None │ ○ None │ │
│ │ LORA │ ● Full │ ● Full │ ○ None │ ◐ Part │ ◐ Part │ │
│ │ TextualInv │ ● Full │ ● Full │ ◐ Part │ ◐ Part │ ◐ Part │ │
│ │ VAE │ ● Full │ ● Full │ ○ None │ ◐ Part │ ◐ Part │ │
│ └──────────────┴────────┴────────┴──────────┴─────────┴──────────┘ │
│ │
│ ● Full = full support, ◐ Part = partial, ○ None = not compatible │
└─────────────────────────────────────────────────────────────────────┘
Implementation Plan
Phase 0: Constants Restructuring ✅ COMPLETED
- ✅ Create new constants file with database-like structure
- ✅ Define TypeScript types matching planned Prisma schema
- ✅ Consolidate all base model data (families, groups, base models, licenses)
- ✅ Implement generation support matrix with numeric IDs
- ✅ Add helper functions for common queries
- ✅ Use numeric auto-incrementing IDs (matching database pattern)
File created: src/shared/constants/basemodel.constants.ts
Phase 1: Database Schema & Migration
- Create Prisma schema for new tables (use schema from this doc)
- Write migration to create tables
- Write seed script to populate from
basemodel.constants.ts - Verify data integrity against constants
Phase 2: Service Layer
- Create
BaseModelServicewith caching - Implement queries:
getBaseModelConfig()- All config datagetBaseModelsByGroup(group)- Base models in a groupgetGenerationSupport(group, modelType)- Compatible base modelsgetBaseModelLicense(baseModel)- License info
- Add cache invalidation on updates
Phase 3: API Endpoints
- Create tRPC router for base model management
- Admin endpoints: CRUD for families, groups, base models, licenses
- Public endpoints: Read-only config data
- Add proper authorization (admin-only for mutations)
Phase 4: Client Integration
- Implement script injection in
_document.tsx - Create
useBaseModelConfig()hook - Migrate components from constants to service/hook
- Add background refresh for long sessions
Phase 5: Admin UI
- Build family/group/base model list views
- Build create/edit forms
- Build generation support matrix editor
- Add validation and preview
Phase 6: Deprecate Constants
- Remove usage of old constants
- Keep constants as fallback during transition
- Remove constants files once fully migrated
Queries
Get compatible base models for generation
async function getCompatibleBaseModels(
targetBaseModel: string,
modelType: ModelType
): Promise<{ full: BaseModel[]; partial: BaseModel[] }> {
// Get the group of the target base model
const target = await prisma.baseModel.findUnique({
where: { key: targetBaseModel },
include: { group: true },
});
// Get all base models with full or partial support for this group + modelType
const compatible = await prisma.baseModelGenerationSupport.findMany({
where: {
groupId: target.groupId,
modelType: modelType,
},
include: { baseModel: true },
});
return {
full: compatible.filter(c => c.support === 'full').map(c => c.baseModel),
partial: compatible.filter(c => c.support === 'partial').map(c => c.baseModel),
};
}
Get generation matrix for a group
async function getGenerationMatrix(groupKey: string) {
const records = await prisma.baseModelGenerationSupport.findMany({
where: { group: { key: groupKey } },
include: { baseModel: true },
orderBy: [{ modelType: 'asc' }, { baseModel: { sortOrder: 'asc' } }],
});
// Pivot into matrix form
const matrix: Record<ModelType, Record<string, SupportLevel>> = {};
for (const record of records) {
if (!matrix[record.modelType]) {
matrix[record.modelType] = {};
}
matrix[record.modelType][record.baseModel.key] = record.support;
}
return matrix;
}
Get all config for client
async function getBaseModelConfigForClient() {
const [families, groups, baseModels, generationSupport] = await Promise.all([
prisma.baseModelFamily.findMany({ orderBy: { sortOrder: 'asc' } }),
prisma.baseModelGroup.findMany({
orderBy: { sortOrder: 'asc' },
include: { baseModels: { select: { key: true } } },
}),
prisma.baseModel.findMany({
where: { hidden: false },
orderBy: { sortOrder: 'asc' },
include: { license: true },
}),
prisma.baseModelGenerationSupport.findMany(),
]);
return { families, groups, baseModels, generationSupport };
}
Open Questions
- How long should the cache TTL be? (Suggested: 5 minutes)
- Should we version the configuration for rollback capabilities? - @dev - yes
- Should changes require approval workflow or take effect immediately? - @dev - the ui we will end up designing will allow the user making the changes to request review.
- How do we handle in-flight generations when config changes? - @dev - it will happen in phases. First, we'll update the base model constants to more closely align with the db models we have planned.
Resolved Items
@dev - each base model group also needs to have a modelVersionId associated with it. This modelVersionId will be the default model version associated with that base model.
@ai: ✅ Done - Added modelVersionId to BaseModelGroupRecord type and populated for groups that have default checkpoints.
@dev - Before we make any changes to the prisma schema or anything else, I'd like to have all the constants that we're working with be put into a new constants file. The constants need to have the same structure that we're planning for the db. Where the data requires ids, create ids for those records. the file can be /shared/constants/basemodel.constants.ts
@ai: ✅ Done - Created src/shared/constants/basemodel.constants.ts with:
- 24 licenses (IDs 1-24)
- 12 families (IDs 1-12)
- 49 groups (IDs 1-49) with settings and modelVersionId
- 66 base models (IDs 1-66) with canGenerate/canTrain/canAuction/deprecated flags
- ~300+ generation support records with auto-incrementing IDs
- Helper functions for common queries
- Map-based lookups by ID and key