Overview

Alchemy uses alchemical metaphors to describe the process of transforming inputs through an LLM into structured outputs. Each concept maps to a specific responsibility in the pipeline:

ConceptRoleAnalogy
MaterialInput contentRaw ingredients
TransformPreprocessing pipelinePreparation step
SpellThe prompt / instructionIncantation
TransmuterLLM provider adapterThe furnace
RefinerOutput parser / validatorPurification
RecipeComplete formulaAlchemical formula
AlchemistOrchestratorThe alchemist

Material

Materials are the inputs to your recipes. Alchemy supports six built-in material types, making it natively multimodal:

TypeDescriptionKey Fields
textPlain text contenttext
imageImage via URL or base64source: { kind: "url" | "base64" }
audioAudio contentsource: { kind: "url" | "base64" }
documentDocument text or URLsource: { kind: "url" | "text" }
videoVideo contentsource: { kind: "url" | "base64" }
dataStructured data (CSV, JSON, TSV)format, content, label?
ts
// Multiple material types in a single request
const materials = [
  { type: "text", text: "Analyze this chart and data" },
  { type: "image", source: { kind: "url", url: "https://example.com/chart.png" } },
  { type: "data", format: "csv", content: "name,value\nA,10\nB,20" },
];

The MaterialPartRegistry interface allows you to extend Material with custom types for domain-specific needs.

Transform

Transforms are preprocessing functions that modify materials before they reach the LLM. They run in the pipeline between input and the transmuter.

ts
import {
  truncateText,
  prependText,
  filterByType,
  dataToText,
} from "@edv4h/alchemy-core";
import { OpenAITransmuter } from "@edv4h/alchemy-plugin-transmuter-openai";

// Built-in transforms
const alchemist = new Alchemist({
  transmuter: new OpenAITransmuter({ apiKey: "..." }),
  transforms: [
    truncateText(4000),     // Limit text length
    dataToText(),           // Convert data parts to readable text
  ],
});

Node.js-specific transforms are available in @edv4h/alchemy-plugin-transforms-node:

  • imageUrlToBase64() — Fetches remote images and converts to base64
  • documentToText() — Extracts text from documents
  • audioToText() — Audio transcription (requires Whisper integration)
  • videoToFrames() — Video frame extraction (requires ffmpeg)

Spell

A Spell is the prompt instruction — it tells the LLM what to do with the materials. Defined as part of the SpellOutput type, it can be a simple string or an array of material parts:

ts
// Simple text spell — a function returning SpellOutput
const recipe = {
  spell: () => "Summarize this document in 3 sentences",
  // ...
};

// The spell output is appended to the materials before transmutation

Transmuter

A Transmuter is the LLM provider adapter — it translates Alchemy's internal format into API calls. The Transmuter interface has two methods:

ts
interface Transmuter {
  transmute(
    materials: MaterialPart[],
    options: TransmutationOptions,
  ): Promise<TransmutationResult>;

  stream(
    materials: MaterialPart[],
    options: TransmutationOptions,
  ): AsyncGenerator<string>;
}

Alchemy provides transmuter plugins for popular LLM providers: @edv4h/alchemy-plugin-transmuter-openai, @edv4h/alchemy-plugin-transmuter-anthropic, and @edv4h/alchemy-plugin-transmuter-google. You can also implement custom transmuters for any provider.

Custom Transmuter

Implement the Transmuter interface to add support for any LLM. Map MaterialPart[] to your provider's message format and return a TransmutationResult.

Refiner

Refiners parse and validate the raw LLM output into your desired format. Alchemy includes two built-in refiners:

TextRefiner

Simply trims whitespace from the output text.

JsonRefiner

The most powerful refiner — validates output against a Zod schema:

ts
import { JsonRefiner } from "@edv4h/alchemy-core";
import { z } from "zod";

const refiner = new JsonRefiner(z.object({
  title: z.string(),
  tags: z.array(z.string()),
  score: z.number().min(0).max(100),
}));

// The refiner:
// 1. Adds format instructions to the prompt automatically
// 2. Strips markdown code fences from the response
// 3. Parses JSON
// 4. Validates against the Zod schema
// 5. Returns a fully typed object

Recipe

A Recipe combines all the above into a single, reusable formula:

ts
import type { Recipe } from "@edv4h/alchemy-core";

const summarizeRecipe: Recipe<string, { summary: string; bullets: string[] }> = {
  spell: () => "Summarize this text",
  refiner: new JsonRefiner(z.object({
    summary: z.string(),
    bullets: z.array(z.string()),
  })),
  roleDefinition: "You are an expert summarizer",
  temperature: 0.3,
  transforms: [truncateText(8000)],
};

Recipes are generic over TInput and TOutput, giving you end-to-end type safety from input to output.

Material Requirements

Recipes can declare what types of materials they expect using requiredMaterials. This enables validation before transmutation, preventing wasted API calls on invalid input.

ts
const imageAnalysisRecipe: Recipe<MaterialPart[], AnalysisResult> = {
  id: "image-analysis",
  spell: (material) => ({ output: "Analyze the provided images" }),
  refiner: new JsonRefiner(AnalysisSchema),
  // Declarative: require 1-5 images
  requiredMaterials: [
    { type: "image", min: 1, max: 5, label: "Analysis images" },
  ],
  // Custom: ensure at least one text description is included
  validateMaterials: (parts) => {
    const hasText = parts.some((p) => p.type === "text");
    return hasText
      ? { valid: true }
      : { valid: false, message: "Please include a text description" };
  },
};

Quality Scoring (evaluate & judgeMaterials)

Beyond presence/count checks, you can score each material's quality with evaluate and make aggregate decisions with judgeMaterials:

ts
const recipe: Recipe<MaterialPart[], BrewResult> = {
  id: "find-theme",
  requiredMaterials: [
    {
      type: "data", min: 1, label: "Engagement score",
      evaluate: (parts) => {
        const data = JSON.parse((parts[0] as DataMaterialPart).content);
        return {
          score: data.responseRate,
          message: data.responseRate < 0.5 ? "Low response rate" : undefined,
        };
      },
    },
  ],
  judgeMaterials: (evaluations) => {
    const failed = evaluations.filter((e) => e.evaluation.score < 0.3);
    if (failed.length > 0) {
      return { canTransmute: false, message: `${failed[0].label} quality is insufficient` };
    }
    return { canTransmute: true };
  },
  // ...
};

Validation runs up to four stages via runMaterialValidation() (async). Each stage runs only when configured, and is skipped if a previous stage fails:

  1. Declarative check — validates requiredMaterials counts by type (if requiredMaterials is set)
  2. Evaluate — runs each requirement's evaluate() in parallel, producing 0–1 quality scores (only for requirements with evaluate)
  3. Judge — passes evaluations to judgeMaterials() to decide transmute eligibility (only if evaluations exist and judgeMaterials is set)
  4. Custom check — runs validateMaterials() as a final filter (if provided)

Alchemist

The Alchemist is the main orchestrator. It wires everything together and executes the transmutation pipeline:

ts
import { Alchemist, dataToText } from "@edv4h/alchemy-node";
import { OpenAITransmuter } from "@edv4h/alchemy-plugin-transmuter-openai";

const alchemist = new Alchemist({
  transmuter: new OpenAITransmuter({ apiKey: "..." }),
  transforms: [dataToText()],       // Global transforms
  validateMaterials: true,           // Auto-validate before each transmute/stream
});

// Three ways to use the Alchemist:

// 1. transmute — single execution, returns TOutput
const result = await alchemist.transmute(recipe, materials);

// 2. stream — progressive output
for await (const chunk of alchemist.stream(recipe, materials)) {
  process.stdout.write(chunk);
}

// 3. generate — multiple variations
const variations = await alchemist.generate(recipe, materials, 3);
// Returns Record<string, TOutput | { error: Error }>
Pipeline Order

Material → Global Transforms → Recipe Transforms → Spell + Format Instructions → Transmuter → Refiner → Typed Output