Concepts
Understand the alchemical metaphors that power the Alchemy framework.
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:
| Concept | Role | Analogy |
|---|---|---|
| Material | Input content | Raw ingredients |
| Transform | Preprocessing pipeline | Preparation step |
| Spell | The prompt / instruction | Incantation |
| Transmuter | LLM provider adapter | The furnace |
| Refiner | Output parser / validator | Purification |
| Recipe | Complete formula | Alchemical formula |
| Alchemist | Orchestrator | The alchemist |
Material
Materials are the inputs to your recipes. Alchemy supports six built-in material types, making it natively multimodal:
| Type | Description | Key Fields |
|---|---|---|
text | Plain text content | text |
image | Image via URL or base64 | source: { kind: "url" | "base64" } |
audio | Audio content | source: { kind: "url" | "base64" } |
document | Document text or URL | source: { kind: "url" | "text" } |
video | Video content | source: { kind: "url" | "base64" } |
data | Structured data (CSV, JSON, TSV) | format, content, label? |
// 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.
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 base64documentToText()— Extracts text from documentsaudioToText()— 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:
// 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:
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.
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:
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:
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.
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:
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:
- Declarative check — validates
requiredMaterialscounts by type (ifrequiredMaterialsis set) - Evaluate — runs each requirement's
evaluate()in parallel, producing 0–1 quality scores (only for requirements withevaluate) - Judge — passes evaluations to
judgeMaterials()to decide transmute eligibility (only if evaluations exist andjudgeMaterialsis set) - 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:
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 }>
Material → Global Transforms → Recipe Transforms → Spell + Format Instructions → Transmuter → Refiner → Typed Output