Disciplines · Integrations

Civitai Best Practices

Civitai uses "Buzz" as its credit system.

9sections1 minread

On this page

This guide covers optimization strategies, cost management, and production recommendations for using Civitai in the Oshun platform.

Cost Optimization#

Understanding Buzz#

Civitai uses "Buzz" as its credit system. Understanding cost factors helps optimize spending.

Image Generation Cost Factors#

Factor Impact Optimization
Base Model SD1=2, SDXL=5, Flux=8 Buzz Use SD1.5 for prototyping
LoRAs/Networks +0.5-2 Buzz per network Minimize network count
Batch Size Linear multiplier Batch similar requests
Draft Mode 40% savings on SDXL Use for previews

Cost Optimization Strategies#

typescript
// 1. Use draft mode for previews
const preview = await provider.generateImageWithPreset(
  model,
  prompt,
  'fast' // 12 steps, lower CFG
);

// 2. Batch similar generations
const jobs = await Promise.all([
  provider.generateImage({ model, params: { prompt: 'A' }, batchSize: 4 }),
  // Instead of 4 separate requests
]);

// 3. Cache generated images
const cacheKey = generateCacheKey(prompt, model, seed);
const cached = await cache.get(cacheKey);
if (!cached) {
  const result = await provider.generateImageAndWait(request);
  await cache.set(cacheKey, result);
}

// 4. Track and budget costs
const budget = 1000; // Buzz
let spent = 0;

provider.on('cost:recorded', ({ cost }) => {
  spent += cost;
  if (spent > budget * 0.8) {
    console.warn('80% of budget used!');
  }
});

Video Generation Cost Management#

typescript
// 1. Start with shorter durations
const testVideo = await provider.generateVideo({
  model: 'wan-2.1',
  params: {
    prompt: 'Test motion',
    duration: 2, // Start short
    quality: 'draft', // 50% cost
  },
});

// 2. Use appropriate models
// ltxv/mochi = 8 Buzz/s, veo3 = 25 Buzz/s
const economicalJob = await provider.generateVideo({
  model: 'ltxv', // Most economical
  params: { prompt: 'Simple animation', duration: 4 },
});

// 3. Validate prompts before generation
function validateVideoPrompt(prompt: string): boolean {
  // Ensure prompt is specific enough
  return prompt.length > 20 && prompt.includes('motion');
}

Training Cost Optimization#

typescript
import {
  estimateTrainingCost,
  calculateTotalSteps,
} from '@oshun/civitai-training';

// 1. Estimate before training
const steps = calculateTotalSteps(images.length, repeats, batchSize, epochs);
const estimatedCost = estimateTrainingCost('sdxl', steps);

if (estimatedCost > budget) {
  // Reduce epochs or images
  epochs = Math.floor(epochs * (budget / estimatedCost));
}

// 2. Use smaller ranks for styles (less training needed)
const styleConfig = {
  network: { rank: 32 }, // Not 128
  epochs: 10, // Not 20
};

// 3. Start with fast-test preset
const testJob = await training.startTrainingWithPreset(
  'fast-test', // 3 epochs, rank 8
  dataset,
  'test-run'
);
// Check results before full training

API Optimization#

Rate Limiting Best Practices#

typescript
// 1. Pre-check rate limit state
const rateLimitState = service.getRateLimitState();
if (rateLimitState.remaining < 5) {
  // Wait or use cached data
  await sleep(rateLimitState.getWaitTimeMs());
}

// 2. Implement request queuing
class RequestQueue {
  private queue: Array<() => Promise<void>> = [];
  private processing = false;

  async add<T>(request: () => Promise<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      this.queue.push(async () => {
        try {
          const result = await request();
          resolve(result);
        } catch (error) {
          reject(error);
        }
      });
      this.processQueue();
    });
  }

  private async processQueue(): Promise<void> {
    if (this.processing) return;
    this.processing = true;

    while (this.queue.length > 0) {
      const request = this.queue.shift()!;
      await request();
      await sleep(1000); // Minimum delay between requests
    }

    this.processing = false;
  }
}

// 3. Use event-based rate limit handling
service.on('rate_limited', async ({ waitTime }) => {
  console.log(`Rate limited. Waiting ${waitTime}ms...`);
  await sleep(waitTime);
});

Caching Strategies#

typescript
// 1. Model metadata caching
const modelCache = new Map<number, { model: Model; expiresAt: Date }>();

async function getModelCached(modelId: number): Promise<Model> {
  const cached = modelCache.get(modelId);
  if (cached && cached.expiresAt > new Date()) {
    return cached.model;
  }

  const model = await service.getModel(modelId);
  modelCache.set(modelId, {
    model,
    expiresAt: new Date(Date.now() + 3600000), // 1 hour
  });
  return model;
}

// 2. Search result caching
const searchCache = new RequestCache<ModelInfo[]>(100, 300);

async function searchModelsCached(
  query: ModelSearchQuery
): Promise<ModelInfo[]> {
  const key = RequestCache.generateKey('search', query);
  let results = searchCache.get(key);

  if (!results) {
    const response = await service.listModels(query);
    results = response.items;
    searchCache.set(key, results);
  }

  return results;
}

// 3. Hash-to-version lookup caching
const hashCache = new Map<string, number>(); // hash -> versionId

Error Recovery#

typescript
// 1. Implement exponential backoff
async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries: number = 3,
  baseDelay: number = 1000
): Promise<T> {
  let lastError: Error;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;

      if (!isRetryable(error)) throw error;

      const delay = baseDelay * Math.pow(2, attempt);
      const jitter = Math.random() * delay * 0.1;
      await sleep(delay + jitter);
    }
  }

  throw lastError!;
}

function isRetryable(error: unknown): boolean {
  if (error instanceof CivitaiError) {
    return ['RATE_LIMITED', 'TIMEOUT', 'NETWORK_ERROR'].includes(error.code);
  }
  return false;
}

// 2. Circuit breaker integration
service.on('circuit_open', () => {
  // Switch to fallback provider or queue requests
  useFallbackProvider();
});

// 3. Graceful degradation
async function generateWithFallback(
  request: ImageGenerationRequest
): Promise<JobResult> {
  try {
    return await provider.generateImageAndWait(request);
  } catch (error) {
    if (error instanceof CivitaiError && error.code === 'RATE_LIMITED') {
      // Try with lower priority
      return await fallbackProvider.generateImageAndWait({
        ...request,
        params: { ...request.params, steps: 10 }, // Reduce steps
      });
    }
    throw error;
  }
}

Generation Best Practices#

Prompt Engineering#

typescript
// 1. Structure prompts consistently
interface PromptBuilder {
  subject: string;
  style?: string;
  quality?: string[];
  negative?: string[];
}

function buildPrompt(builder: PromptBuilder): string {
  const parts = [builder.subject];

  if (builder.style) {
    parts.push(builder.style);
  }

  if (builder.quality?.length) {
    parts.push(...builder.quality);
  }

  return parts.join(', ');
}

function buildNegativePrompt(builder: PromptBuilder): string {
  const defaults = ['blurry', 'low quality', 'artifacts', 'distorted'];
  return [...defaults, ...(builder.negative || [])].join(', ');
}

// Usage
const prompt = buildPrompt({
  subject: 'A majestic dragon',
  style: 'digital art',
  quality: ['highly detailed', '8k', 'trending on artstation'],
});

// 2. Model-specific prompts
const modelPromptStyles: Record<string, (base: string) => string> = {
  // SDXL responds well to quality tags
  sdxl: (base) => `${base}, masterpiece, best quality`,

  // Flux prefers natural language
  flux: (base) => `A high quality image of ${base}, photorealistic`,

  // Pony uses specific tags
  pony: (base) => `${base}, score_9, score_8_up`,
};

Seed Management#

typescript
// 1. Track seeds for reproducibility
interface GenerationRecord {
  prompt: string;
  model: string;
  seed: number;
  result: string;
}

const generations: GenerationRecord[] = [];

async function generateTracked(
  request: ImageGenerationRequest
): Promise<JobResult> {
  const seed = request.params.seed ?? Math.floor(Math.random() * 2147483647);

  const job = await provider.generateImageAndWait({
    ...request,
    params: { ...request.params, seed },
  });

  generations.push({
    prompt: request.params.prompt,
    model: request.model,
    seed,
    result: job.blobUrl!,
  });

  return job;
}

// 2. Seed variation for similar results
function generateSeedVariations(baseSeed: number, count: number): number[] {
  return Array.from({ length: count }, (_, i) => baseSeed + i);
}

Quality vs Speed Trade-offs#

typescript
// Quality levels with configurations
const qualityConfigs = {
  preview: {
    steps: 8,
    scheduler: 'LCM' as const,
    cfgScale: 2,
    width: 512,
    height: 512,
  },
  draft: {
    steps: 15,
    scheduler: 'EulerA' as const,
    cfgScale: 5,
    width: 768,
    height: 768,
  },
  standard: {
    steps: 25,
    scheduler: 'DPM2MKarras' as const,
    cfgScale: 7,
    width: 1024,
    height: 1024,
  },
  high: {
    steps: 40,
    scheduler: 'DPM2MKarras' as const,
    cfgScale: 7,
    width: 1024,
    height: 1024,
  },
  ultra: {
    steps: 60,
    scheduler: 'DPMSDEKarras' as const,
    cfgScale: 7,
    width: 1024,
    height: 1024,
  },
};

// Use preview for iterations, standard for final
async function iterativeGeneration(prompt: string): Promise<JobResult> {
  // Quick preview
  const preview = await provider.generateImage({
    model,
    params: { prompt, ...qualityConfigs.preview },
  });

  // If user approves preview, generate high quality
  const final = await provider.generateImage({
    model,
    params: { prompt, ...qualityConfigs.standard, seed: preview.seed },
  });

  return final;
}

Model Selection#

Choosing the Right Base Model#

Use Case Recommended Model Why
Photorealistic portraits Juggernaut XL Best for faces
Anime/illustration Animagine XL Trained on anime
General purpose RealVisXL Versatile
Highest quality Flux Dev Best overall
Fast iteration SD 1.5 Low cost, fast
Video characters Hunyuan Good LoRA support
Video motion Wan 2.1/2.2 Motion training

LoRA Selection#

typescript
// 1. Check LoRA compatibility
function isCompatible(
  loraBaseModel: string,
  checkpointBaseModel: string
): boolean {
  const compatibility: Record<string, string[]> = {
    'SD 1.5': ['SD 1.5', 'SD 1.4'],
    'SDXL 1.0': ['SDXL 1.0', 'SDXL 0.9'],
    Flux: ['Flux.1 D', 'Flux.1 S'],
  };

  return compatibility[checkpointBaseModel]?.includes(loraBaseModel) ?? false;
}

// 2. Stack LoRAs intelligently
function optimizeLoraStack(loras: AdditionalNetwork[]): AdditionalNetwork[] {
  // Sort by importance
  const sorted = [...loras].sort(
    (a, b) => (b.strength ?? 1) - (a.strength ?? 1)
  );

  // Limit to 3-4 LoRAs for best results
  const limited = sorted.slice(0, 4);

  // Reduce total strength if > 1.5
  const totalStrength = limited.reduce((sum, l) => sum + (l.strength ?? 1), 0);
  if (totalStrength > 1.5) {
    const scale = 1.5 / totalStrength;
    return limited.map((l) => ({ ...l, strength: (l.strength ?? 1) * scale }));
  }

  return limited;
}

Training Best Practices#

Dataset Quality#

typescript
// 1. Pre-process images
async function preprocessDataset(
  images: DatasetImage[]
): Promise<DatasetImage[]> {
  return Promise.all(
    images.map(async (img) => {
      // Ensure consistent resolution
      const processed = await resizeImage(img.image, 1024, 1024);

      // Generate caption if missing
      const caption = img.caption ?? (await generateCaption(processed));

      return { ...img, image: processed, caption };
    })
  );
}

// 2. Validate dataset diversity
function analyzeDatasetDiversity(images: DatasetImage[]): {
  issues: string[];
  recommendations: string[];
} {
  const issues: string[] = [];
  const recommendations: string[] = [];

  // Check image count
  if (images.length < 10) {
    issues.push('Dataset too small (< 10 images)');
    recommendations.push('Add more images for better results');
  }

  // Check caption diversity
  const uniqueCaptions = new Set(images.map((i) => i.caption)).size;
  if (uniqueCaptions < images.length * 0.5) {
    issues.push('Low caption diversity');
    recommendations.push('Use more varied captions');
  }

  // Check for repeats balance
  const avgRepeats =
    images.reduce((sum, i) => sum + (i.repeats ?? 1), 0) / images.length;
  if (avgRepeats < 3) {
    recommendations.push('Consider increasing repeats for small datasets');
  }

  return { issues, recommendations };
}

Training Monitoring#

typescript
// 1. Loss monitoring with early stopping
class LossMonitor {
  private history: number[] = [];
  private bestLoss = Infinity;
  private patienceCounter = 0;

  constructor(private patience: number = 5) {}

  update(loss: number): { shouldStop: boolean; reason?: string } {
    this.history.push(loss);

    if (loss < this.bestLoss) {
      this.bestLoss = loss;
      this.patienceCounter = 0;
    } else {
      this.patienceCounter++;
    }

    // Check for overfitting
    if (this.patienceCounter >= this.patience) {
      return { shouldStop: true, reason: 'Loss not improving (overfitting)' };
    }

    // Check for explosion
    if (this.history.length > 10) {
      const recent = this.history.slice(-10);
      const avgRecent = recent.reduce((a, b) => a + b, 0) / recent.length;
      const avgPrevious =
        this.history.slice(-20, -10).reduce((a, b) => a + b, 0) / 10;

      if (avgRecent > avgPrevious * 1.5) {
        return { shouldStop: true, reason: 'Loss exploding' };
      }
    }

    return { shouldStop: false };
  }
}

// 2. Progress reporting
training.on('training:progress', ({ jobId, progress }) => {
  const eta = progress.estimatedTimeRemaining
    ? `${Math.round(progress.estimatedTimeRemaining / 60)}min remaining`
    : 'calculating...';

  console.log(
    [
      `Job: ${jobId}`,
      `Epoch: ${progress.currentEpoch}/${progress.totalEpochs}`,
      `Step: ${progress.currentStep}/${progress.totalSteps}`,
      `Loss: ${progress.loss.currentLoss.toFixed(4)}`,
      `Best: ${progress.loss.bestLoss.toFixed(4)}`,
      `ETA: ${eta}`,
    ].join(' | ')
  );
});

Production Deployment#

Health Monitoring#

typescript
interface HealthStatus {
  healthy: boolean;
  checks: {
    api: { status: 'ok' | 'degraded' | 'down'; latency?: number };
    rateLimit: { status: 'ok' | 'warning' | 'critical'; remaining: number };
    circuitBreaker: { status: 'ok' | 'open'; state: string };
    cache: { hitRate: number };
  };
}

async function checkHealth(): Promise<HealthStatus> {
  const rateLimitState = service.getRateLimitState();
  const circuitState = service.getCircuitBreakerState();
  const cacheStats = service.getCacheStats();

  const checks = {
    api: {
      status:
        circuitState.state === 'open' ? ('down' as const) : ('ok' as const),
    },
    rateLimit: {
      status:
        rateLimitState.remaining < 10
          ? ('critical' as const)
          : rateLimitState.remaining < 30
            ? ('warning' as const)
            : ('ok' as const),
      remaining: rateLimitState.remaining,
    },
    circuitBreaker: {
      status:
        circuitState.state === 'open' ? ('open' as const) : ('ok' as const),
      state: circuitState.state,
    },
    cache: {
      hitRate: cacheStats.hits / (cacheStats.hits + cacheStats.misses) || 0,
    },
  };

  return {
    healthy:
      checks.api.status === 'ok' && checks.circuitBreaker.status === 'ok',
    checks,
  };
}

Metrics Collection#

typescript
// 1. Request metrics
const metrics = {
  requestsTotal: 0,
  requestsSuccessful: 0,
  requestsFailed: 0,
  latencyHistogram: new Array(10).fill(0), // 0-100ms, 100-200ms, etc.
  buzzSpent: 0,
};

service.on('request_complete', ({ duration }) => {
  metrics.requestsTotal++;
  metrics.requestsSuccessful++;

  const bucket = Math.min(Math.floor(duration / 100), 9);
  metrics.latencyHistogram[bucket]++;
});

service.on('request_error', () => {
  metrics.requestsTotal++;
  metrics.requestsFailed++;
});

provider.on('cost:recorded', ({ cost }) => {
  metrics.buzzSpent += cost;
});

// 2. Prometheus-style metrics export
function exportMetrics(): string {
  return [
    `civitai_requests_total ${metrics.requestsTotal}`,
    `civitai_requests_successful ${metrics.requestsSuccessful}`,
    `civitai_requests_failed ${metrics.requestsFailed}`,
    `civitai_buzz_spent_total ${metrics.buzzSpent}`,
    `civitai_cache_hit_rate ${service.getCacheStats().hits / (service.getCacheStats().hits + service.getCacheStats().misses)}`,
  ].join('\n');
}

Alerting#

typescript
// 1. Cost alerting
const dailyBudget = 5000; // Buzz
let dailySpent = 0;

provider.on('cost:recorded', ({ cost }) => {
  dailySpent += cost;

  if (
    dailySpent > dailyBudget * 0.8 &&
    dailySpent - cost <= dailyBudget * 0.8
  ) {
    sendAlert('warning', '80% of daily Buzz budget used');
  }

  if (dailySpent > dailyBudget) {
    sendAlert('critical', 'Daily Buzz budget exceeded');
  }
});

// 2. Error rate alerting
let recentErrors = 0;
let recentRequests = 0;

service.on('request_complete', () => {
  recentRequests++;
});
service.on('request_error', () => {
  recentRequests++;
  recentErrors++;
});

setInterval(() => {
  if (recentRequests > 10) {
    const errorRate = recentErrors / recentRequests;
    if (errorRate > 0.1) {
      sendAlert('warning', `High error rate: ${(errorRate * 100).toFixed(1)}%`);
    }
  }
  recentErrors = 0;
  recentRequests = 0;
}, 60000);

// 3. Circuit breaker alerting
service.on('circuit_open', ({ endpoint }) => {
  sendAlert('critical', `Circuit breaker opened for ${endpoint}`);
});

Security Considerations#

API Key Management#

typescript
// 1. Never hardcode API keys
const apiKey = process.env.CIVITAI_API_KEY;
if (!apiKey) {
  throw new Error('CIVITAI_API_KEY environment variable required');
}

// 2. Rotate keys periodically
// Store key metadata
interface KeyMetadata {
  createdAt: Date;
  lastUsed: Date;
  rotatedAt?: Date;
}

// 3. Audit key usage
service.on('request_start', ({ endpoint }) => {
  auditLog.log({
    action: 'civitai_api_call',
    endpoint,
    timestamp: new Date(),
    keyId: process.env.CIVITAI_KEY_ID,
  });
});

Content Safety#

typescript
// 1. Filter content by rating
const service = createCivitAIService({
  apiKey,
  nsfwEnabled: false, // Disable NSFW content
  contentLevel: 'PG13', // Maximum content level
});

// 2. Validate model content ratings
async function validateModelSafety(modelId: number): Promise<boolean> {
  const model = await service.getModel(modelId);

  if (model.nsfw) {
    console.warn(`Model ${modelId} is NSFW`);
    return false;
  }

  return service.isContentLevelAllowed(model.nsfwLevel);
}

// 3. Content moderation for generated images
async function moderateGeneration(result: JobResult): Promise<boolean> {
  // Implement content moderation
  const isSafe = await contentModerationService.check(result.blobUrl);
  return isSafe;
}

Troubleshooting#

Common Issues#

Issue Cause Solution
Rate limited frequently Too many requests Implement request queuing
High latency Large batch sizes Reduce batch size
Generation failures Model unavailable Use fallback models
Training takes too long Too many epochs Reduce epochs, use presets
LoRA has no effect Low strength Increase LoRA strength
Images look identical Seed reuse Use random seeds

Debugging#

typescript
// 1. Enable verbose logging
service.on('request_start', (e) => console.log('Request:', e));
service.on('request_complete', (e) => console.log('Complete:', e));
service.on('request_error', (e) => console.error('Error:', e));

// 2. Dump state for debugging
function debugDump(): void {
  console.log('=== Civitai Debug Dump ===');
  console.log('Rate Limit:', service.getRateLimitState());
  console.log('Circuit:', service.getCircuitBreakerState());
  console.log('Cache:', service.getCacheStats());
  console.log('Analytics:', service.getAnalytics());
}

// 3. Test connectivity
async function testConnection(): Promise<boolean> {
  try {
    await service.listTags({ limit: 1 });
    return true;
  } catch (error) {
    console.error('Connection test failed:', error);
    return false;
  }
}