Skip to main content
Technology & EngineeringImage Generation Services393 lines

Leonardo AI Image Generation

"Leonardo AI: image generation API, fine-tuned models, canvas editing, texture generation, REST API"

Quick Summary18 lines
Leonardo AI offers a REST API for image generation with a focus on fine-tuned models, game assets, and creative production workflows. The platform provides pre-trained model variants optimized for specific styles (photorealism, anime, concept art, 3D rendering) alongside the ability to train custom models on your own datasets. The API follows an async pattern: you submit a generation request, receive a generation ID, and poll for results. Leonardo differentiates itself with specialized features like texture generation for 3D models, canvas-based editing, and motion generation. The token-based pricing model means each generation costs a set number of tokens based on model, resolution, and features used. Use Leonardo when you need style-consistent output from fine-tuned models or game/creative asset pipelines.

## Key Points

- **Use Alchemy for best quality**: Alchemy is Leonardo's enhanced generation pipeline. Enable it for production-quality output -- the token cost increase is worth the quality gain.
- **Choose models by use case**: Use PhotoReal for photographs, Kino XL for cinematic scenes, Anime Pastel Dream for anime, and DreamShaper for versatile creative work.
- **Batch efficiently**: Submit multiple generation requests and poll them in parallel. Leonardo allows concurrent generations based on your plan tier.
- **Cache model IDs**: Fetch the model list once and cache it. Model IDs are stable and do not change.
- **Use preset styles**: Presets like `CINEMATIC`, `CREATIVE`, `DYNAMIC`, `VIBRANT` adjust generation parameters behind the scenes for consistent aesthetic results.
- **Monitor token usage**: Track token consumption per generation to predict costs. Higher resolution, more images, and Alchemy all increase token costs.
- **Download and store images**: Leonardo-hosted URLs are temporary. Download generated images to your own storage for permanent access.
- **Train on consistent datasets**: For custom models, use 10-20 high-quality, consistent images. Inconsistent training data produces unreliable models.
- **Polling without delay**: Generations take 10-60 seconds. Polling every 500ms wastes API calls and may trigger rate limiting. Use 3-5 second intervals.
- **Ignoring NSFW flags**: Generated images include an `nsfw` field. Always check and filter based on your platform requirements before displaying to users.
- **Using default dimensions for all models**: Different models perform best at different resolutions. Check model documentation for recommended dimensions.
- **Training models with too few images**: Custom model training with fewer than 5 images produces poor results. Use 10-20 curated images minimum.
skilldb get image-generation-services-skills/Leonardo AI Image GenerationFull skill: 393 lines
Paste into your CLAUDE.md or agent config

Leonardo AI Image Generation

Core Philosophy

Leonardo AI offers a REST API for image generation with a focus on fine-tuned models, game assets, and creative production workflows. The platform provides pre-trained model variants optimized for specific styles (photorealism, anime, concept art, 3D rendering) alongside the ability to train custom models on your own datasets. The API follows an async pattern: you submit a generation request, receive a generation ID, and poll for results. Leonardo differentiates itself with specialized features like texture generation for 3D models, canvas-based editing, and motion generation. The token-based pricing model means each generation costs a set number of tokens based on model, resolution, and features used. Use Leonardo when you need style-consistent output from fine-tuned models or game/creative asset pipelines.

Setup

Configure the Leonardo AI REST client:

const LEONARDO_API_BASE = "https://cloud.leonardo.ai/api/rest/v1";

class LeonardoClient {
  private apiKey: string;

  constructor(apiKey: string) {
    this.apiKey = apiKey;
  }

  async request<T>(
    method: "GET" | "POST" | "DELETE",
    endpoint: string,
    body?: Record<string, unknown>
  ): Promise<T> {
    const response = await fetch(`${LEONARDO_API_BASE}${endpoint}`, {
      method,
      headers: {
        Authorization: `Bearer ${this.apiKey}`,
        "Content-Type": "application/json",
        Accept: "application/json",
      },
      body: body ? JSON.stringify(body) : undefined,
    });

    if (!response.ok) {
      const error = await response.json().catch(() => ({ message: response.statusText }));
      throw new Error(`Leonardo API error (${response.status}): ${JSON.stringify(error)}`);
    }

    return response.json() as Promise<T>;
  }
}

const leonardo = new LeonardoClient(process.env.LEONARDO_API_KEY!);

Verify your setup by fetching user info:

async function getUserInfo(): Promise<{ id: string; tokenBalance: number }> {
  const data = await leonardo.request<any>("GET", "/me");
  return {
    id: data.user_details[0].user.id,
    tokenBalance: data.user_details[0].tokenRenewalDate
      ? data.user_details[0].apiConcurrencySlots
      : 0,
  };
}

Key Techniques

Text-to-Image Generation

interface GenerationRequest {
  prompt: string;
  negativePrompt?: string;
  modelId?: string;
  width?: number;
  height?: number;
  numImages?: number;
  guidanceScale?: number;
  numInferenceSteps?: number;
  seed?: number;
  presetStyle?: string;
  alchemy?: boolean;
  photoReal?: boolean;
  photoRealVersion?: string;
}

interface GenerationResult {
  id: string;
  images: Array<{
    id: string;
    url: string;
    nsfw: boolean;
  }>;
  status: string;
}

async function generateImages(options: GenerationRequest): Promise<string> {
  const body: Record<string, unknown> = {
    prompt: options.prompt,
    negative_prompt: options.negativePrompt ?? "low quality, blurry, distorted",
    modelId: options.modelId,
    width: options.width ?? 1024,
    height: options.height ?? 1024,
    num_images: options.numImages ?? 1,
    guidance_scale: options.guidanceScale ?? 7,
    num_inference_steps: options.numInferenceSteps,
    seed: options.seed,
    presetStyle: options.presetStyle ?? "DYNAMIC",
    alchemy: options.alchemy ?? true,
    photoReal: options.photoReal ?? false,
    photoRealVersion: options.photoRealVersion,
  };

  // Remove undefined fields
  Object.keys(body).forEach((key) => {
    if (body[key] === undefined) delete body[key];
  });

  const data = await leonardo.request<any>("POST", "/generations", body);
  return data.sdGenerationJob.generationId as string;
}

async function getGeneration(generationId: string): Promise<GenerationResult> {
  const data = await leonardo.request<any>("GET", `/generations/${generationId}`);
  const gen = data.generations_by_pk;

  return {
    id: gen.id,
    status: gen.status,
    images: (gen.generated_images ?? []).map((img: any) => ({
      id: img.id,
      url: img.url,
      nsfw: img.nsfw,
    })),
  };
}

Poll for Completion

async function generateAndWait(
  options: GenerationRequest,
  timeoutMs: number = 120_000
): Promise<GenerationResult> {
  const generationId = await generateImages(options);
  const startTime = Date.now();

  while (Date.now() - startTime < timeoutMs) {
    await new Promise((r) => setTimeout(r, 3000));

    const result = await getGeneration(generationId);

    if (result.status === "COMPLETE") {
      return result;
    }
    if (result.status === "FAILED") {
      throw new Error(`Generation ${generationId} failed`);
    }
  }

  throw new Error(`Generation ${generationId} timed out after ${timeoutMs}ms`);
}

// Usage
const result = await generateAndWait({
  prompt: "A fantasy castle on a floating island, concept art, detailed",
  width: 1024,
  height: 768,
  numImages: 4,
  alchemy: true,
  presetStyle: "CREATIVE",
});

for (const image of result.images) {
  console.log(`Image: ${image.url}`);
}

PhotoReal Mode

Leonardo's PhotoReal mode produces photorealistic images with enhanced detail:

async function generatePhotoReal(
  prompt: string,
  options?: { depth?: number; width?: number; height?: number }
): Promise<GenerationResult> {
  return generateAndWait({
    prompt,
    photoReal: true,
    photoRealVersion: "v2",
    alchemy: true,
    width: options?.width ?? 1024,
    height: options?.height ?? 1024,
    numImages: 1,
    presetStyle: "CINEMATIC",
  });
}

Using Fine-Tuned Platform Models

Leonardo provides pre-trained models optimized for different styles:

// Well-known Leonardo platform model IDs
const LEONARDO_MODELS = {
  KINO_XL: "aa77f04e-3eec-4034-9c07-d0f619684628",
  ANIME_PASTEL_DREAM: "1e60896f-3c26-4296-8ecc-53e2afecc132",
  DREAM_SHAPER_V7: "ac614f96-1082-45bf-be9d-757f2d31c174",
  LEONARDO_DIFFUSION_XL: "1e60896f-3c26-4296-8ecc-53e2afecc132",
} as const;

async function generateAnime(prompt: string): Promise<GenerationResult> {
  return generateAndWait({
    prompt,
    modelId: LEONARDO_MODELS.ANIME_PASTEL_DREAM,
    width: 1024,
    height: 1024,
    numImages: 1,
    guidanceScale: 7,
    presetStyle: "ANIME",
  });
}

async function generateCinematic(prompt: string): Promise<GenerationResult> {
  return generateAndWait({
    prompt,
    modelId: LEONARDO_MODELS.KINO_XL,
    width: 1536,
    height: 1024,
    numImages: 1,
    alchemy: true,
    presetStyle: "CINEMATIC",
  });
}

Training Custom Models

async function createDataset(name: string): Promise<string> {
  const data = await leonardo.request<any>("POST", "/datasets", {
    name,
    description: `Training dataset: ${name}`,
  });
  return data.insert_datasets_one.id;
}

async function uploadTrainingImage(
  datasetId: string,
  imageUrl: string
): Promise<void> {
  // Get a presigned upload URL
  const upload = await leonardo.request<any>(
    "POST",
    `/datasets/${datasetId}/upload`,
    { extension: "jpg" }
  );

  // Upload image to presigned URL
  const imageResponse = await fetch(imageUrl);
  const imageBuffer = await imageResponse.arrayBuffer();

  await fetch(upload.uploadDatasetImage.url, {
    method: "PUT",
    headers: { "Content-Type": "image/jpeg" },
    body: imageBuffer,
  });

  // Confirm the upload
  await leonardo.request("POST", `/datasets/${datasetId}/upload/gen`, {
    id: upload.uploadDatasetImage.id,
  });
}

async function trainModel(
  name: string,
  datasetId: string,
  options?: { resolution?: number; numTrainEpochs?: number }
): Promise<string> {
  const data = await leonardo.request<any>("POST", "/models", {
    name,
    datasetId,
    instance_prompt: `a photo of sks ${name.toLowerCase()}`,
    description: `Fine-tuned model: ${name}`,
    modelType: "GENERAL",
    sd_Version: "SDXL_0_9",
    resolution: options?.resolution ?? 1024,
    num_train_epochs: options?.numTrainEpochs ?? 30,
  });

  return data.sdTrainingJob.customModelId;
}

Texture Generation for 3D Models

async function generateTexture(
  prompt: string,
  modelAssetUrl?: string
): Promise<GenerationResult> {
  const body: Record<string, unknown> = {
    prompt,
    preview: false,
    seed: undefined,
    negative_prompt: "low quality, tiling artifacts",
  };

  if (modelAssetUrl) {
    body.modelAssetUrl = modelAssetUrl;
  }

  const data = await leonardo.request<any>("POST", "/generations-texture", body);
  const generationId = data.textureGenerationJob.id;

  // Poll for texture completion
  const startTime = Date.now();
  while (Date.now() - startTime < 120_000) {
    await new Promise((r) => setTimeout(r, 5000));

    const result = await leonardo.request<any>(
      "GET",
      `/generations-texture/${generationId}`
    );

    if (result.model_asset_texture_generations?.[0]?.status === "COMPLETE") {
      return {
        id: generationId,
        status: "COMPLETE",
        images: result.model_asset_texture_generations[0].texture_urls.map(
          (url: string, i: number) => ({ id: `tex-${i}`, url, nsfw: false })
        ),
      };
    }
  }

  throw new Error("Texture generation timed out");
}

Image Upscaling

async function upscaleImage(imageId: string): Promise<string> {
  const data = await leonardo.request<any>("POST", "/variations/upscale", {
    id: imageId,
  });

  const variationId = data.sdUpscaleJob.id;

  // Poll for upscaled result
  const startTime = Date.now();
  while (Date.now() - startTime < 60_000) {
    await new Promise((r) => setTimeout(r, 3000));
    const result = await leonardo.request<any>(
      "GET",
      `/variations/${variationId}`
    );

    if (result.generated_image_variation_generic?.[0]?.status === "COMPLETE") {
      return result.generated_image_variation_generic[0].url;
    }
  }

  throw new Error("Upscale timed out");
}

Best Practices

  • Use Alchemy for best quality: Alchemy is Leonardo's enhanced generation pipeline. Enable it for production-quality output -- the token cost increase is worth the quality gain.
  • Choose models by use case: Use PhotoReal for photographs, Kino XL for cinematic scenes, Anime Pastel Dream for anime, and DreamShaper for versatile creative work.
  • Batch efficiently: Submit multiple generation requests and poll them in parallel. Leonardo allows concurrent generations based on your plan tier.
  • Cache model IDs: Fetch the model list once and cache it. Model IDs are stable and do not change.
  • Use preset styles: Presets like CINEMATIC, CREATIVE, DYNAMIC, VIBRANT adjust generation parameters behind the scenes for consistent aesthetic results.
  • Monitor token usage: Track token consumption per generation to predict costs. Higher resolution, more images, and Alchemy all increase token costs.
  • Download and store images: Leonardo-hosted URLs are temporary. Download generated images to your own storage for permanent access.
  • Train on consistent datasets: For custom models, use 10-20 high-quality, consistent images. Inconsistent training data produces unreliable models.

Anti-Patterns

  • Polling without delay: Generations take 10-60 seconds. Polling every 500ms wastes API calls and may trigger rate limiting. Use 3-5 second intervals.
  • Ignoring NSFW flags: Generated images include an nsfw field. Always check and filter based on your platform requirements before displaying to users.
  • Using default dimensions for all models: Different models perform best at different resolutions. Check model documentation for recommended dimensions.
  • Training models with too few images: Custom model training with fewer than 5 images produces poor results. Use 10-20 curated images minimum.
  • Skipping negative prompts: Negative prompts significantly improve output quality. Always include at least basic quality exclusions like "low quality, blurry, distorted".
  • Not handling generation failures: Generations can fail due to content moderation, capacity limits, or model issues. Always check the status field and handle failures gracefully.
  • Overusing PhotoReal mode: PhotoReal costs more tokens and is slower. Only use it when photorealism is specifically required. Standard Alchemy mode handles most creative use cases well.

Install this skill directly: skilldb add image-generation-services-skills

Get CLI access →