Structured output generation for LLMs in Node.js. Token-level constraints for local models, native JSON modes for cloud APIs — one unified API.
npm install @aviasole/shapecraft zod
# or: pnpm add @aviasole/shapecraft zodzod is an optional peer dependency, used for the Zod schema examples throughout this README - install it too if you're using Zod schemas (the common path): npm install zod.
Install backend SDK as needed:
npm install openai # OpenAI, Fireworks, Mistral, OpenRouter, DeepSeek (all OpenAI-compatible)
npm install groq-sdk # Groq
npm install @anthropic-ai/sdk # Anthropic
npm install @google/genai # Gemini
npm install node-llama-cpp # Local .gguf models
# Ollama: no extra SDK neededimport { z } from "zod";
import { generate, openai } from "@aviasole/shapecraft";
const PersonSchema = z.object({
name: z.string(),
age: z.number(),
email: z.string().email(),
});
const model = openai({ model: "gpt-4o-mini" });
const result = await generate(model, PersonSchema, "Extract: John Doe, 32, john@example.com");
console.log(result.data); // { name: "John Doe", age: 32, email: "john@example.com" }
console.log(result.guaranteeLevel); // "native"
console.log(result.attempts); // 1Full guide and reference docs — schema inputs, backends & guarantee levels, streaming, createClient() & middleware, batch generation, result metadata, timeouts, pluggable validation, the staged validation pipeline, FHIR presets, turnaround, skill-based generation, multi-agent orchestration, tool calling, the CLI, and error handling: aviasoletechnologies.github.io/shapecraft
| Backend | Guarantee | Mechanism |
|---|---|---|
openai() |
native |
Server-side strict JSON schema |
groq() |
native |
JSON mode |
deepseek() |
native |
JSON mode (no schema-strict mode, same tier as groq()) |
fireworks() |
native |
Server-side JSON schema mode, plus a real token-level GBNF grammar mode |
mistral() |
native |
Server-side JSON schema mode |
gemini() |
native |
Server-side JSON schema mode (responseJsonSchema) |
ollama() |
constrained |
Token-level JSON-schema constraint |
llamaCpp() |
constrained |
Token-level GBNF grammar (local .gguf via node-llama-cpp) |
anthropic() |
best-effort |
Prompt + parse + retry |
openRouter() |
best-effort |
Pass-through to many providers - response_format support varies by underlying model |
llamaCpp()isconstrainedfor a{ gbnf }input (token-level). For other schema types (Zod / jsonSchema / …) it currently runs a best-effort prompt path until the JSON-Schema→GBNF converter lands — treat those as best-effort despite the nominal level.
fireworks()is the one cloud backend where a{ gbnf }input is not downgraded to best-effort — Fireworks' grammar mode (response_format: { type: "grammar", grammar }) applies the GBNF grammar as a genuine token-level constraint server-side, the same guaranteellamaCpp()gives locally. It reuses theopenaipackage pointed at Fireworks' base URL, so no extra SDK dependency is needed.
openRouter()is deliberatelybest-effort, notnativelike the other cloud backends — it's pass-through across many different underlying providers/models, andresponse_format: { type: "json_schema" }enforcement isn't guaranteed for every model it can route to, only the ones that actually support it themselves.
gemini()uses the official@google/genaiSDK, not an OpenAI-compatible endpoint (unlikefireworks()/mistral()/openRouter()) — Gemini's OpenAI-compat layer is a migration bridge for OpenAI users, not its primary integration path, and doesn't exposeresponseJsonSchema(plain JSON Schema, whattoJsonSchema()already produces) — only the olderresponseSchema(Gemini's own Type-enum OpenAPI-subset shape).
import { openai, groq, fireworks, mistral, gemini, openRouter, deepseek, ollama, anthropic, llamaCpp } from "@aviasole/shapecraft";
const gpt = openai({ model: "gpt-4o-mini" });
const fast = groq({ model: "llama-3.3-70b-versatile" });
const cloudGbnf = fireworks({ model: "accounts/fireworks/models/llama-v3p1-70b-instruct" });
const mist = mistral({ model: "mistral-large-latest" });
const gem = gemini({ model: "gemini-flash-latest" });
const router = openRouter({ model: "openai/gpt-4o-mini" });
const deep = deepseek({ model: "deepseek-v4-flash" });
const local = ollama({ model: "llama3.2" });
const native = llamaCpp({ modelPath: "./models/llama-3.2-3b.gguf" });
const claude = anthropic({ model: "claude-haiku-4-5-20251001", maxRetries: 3 });Every built-in backend also exposes capabilities (streaming/chat/structuredOutput/toolCalling/skillDispatch) - an explicit, inspectable alternative to duck-typing typeof model.generateStream === "function" for routing logic. See the Backends & Guarantee Levels guide for the full mechanism breakdown and the ModelCapabilities interface, and what shapecraft guarantees and what it doesn't for the structural-vs-semantic-correctness distinction.
Other libraries solve overlapping parts of this problem well. This is what's actually different, not a scorecard:
| Capability | Instructor-js | zod-gpt | Vercel AI SDK (generateObject) |
shapecraft |
|---|---|---|---|---|
| Providers | OpenAI only | OpenAI, Anthropic | OpenAI, Anthropic, Google, and more | OpenAI, Groq, Fireworks, Mistral, OpenRouter, DeepSeek, Gemini, Anthropic, Ollama, llama.cpp |
| Local model support | - | - | no grammar-level constraint | Ollama / llama.cpp with token-level GBNF grammar |
| Per-provider reliability signal | - | - | - | guaranteeLevel: native / constrained / best-effort |
| Retry on schema failure | not documented | fixed 3 attempts, 60s timeout | configurable maxRetries |
configurable, only on schema-validation failure |
| Timeout / cancellation | not documented | hardcoded 60s | via provider fetch options | timeoutMs / AbortSignal, enforced at the core regardless of backend |
| Streaming | yes | - | yes (streamObject) |
yes, with per-field incremental validation |
| Schema input types | Zod only | Zod only | Zod, Valibot, JSON schema | Zod, JSON schema, regex, custom validator, XML, GBNF |
The gap that actually matters: none of the others tell you how much to trust a given provider's structured output, or give local models the same real enforcement cloud providers get. shapecraft's guaranteeLevel makes that explicit instead of leaving it as something you find out in production.
- Schema inputs: Zod, raw JSON Schema, regex pattern, custom validator, XML (with template placeholders and
enforceLiterals), raw GBNF grammar - Streaming:
generateStream()with per-field incremental validation, visible retries on validation failure createClient()& middleware: a Koa-style onion pipeline for logging, caching, and other cross-cutting concerns- Batch generation:
generateBatch(), concurrency-capped,Promise.allSettled-style - Result metadata:
provider,model,latencyMson every result - Timeouts & cancellation:
timeoutMs/AbortSignal, enforced at the core for every backend - Pluggable JSON Schema validation: swap in your own validator (or AJV) via
jsonSchemaValidator - Staged validation pipeline:
semanticValidator,confidenceScorer,postProcessorson top of structural validation - FHIR R4 presets:
Patient,Observation,Condition,MedicationRequest,Encounter, via@aviasole/shapecraft/fhir, including a commonextension?: Extension[]field - Turnaround: multi-turn collection via
generate(..., { turnaround: true }), validated once over the whole transcript - Skill-based generation:
SkillRegistry+generateSkillCall()/runSkillLoop()for model-driven dispatch to typed operations - Multi-agent orchestration:
defineAgent()+runAgents()for chaining validatedgenerate()calls, via@aviasole/shapecraft/agentic - Tool calling:
generateWithTools()for native provider function-calling with validated arguments and a validated final answer - CLI:
npx shapecraft validate --schema schema.json --output output.json
Full docs for all of the above: aviasoletechnologies.github.io/shapecraft
import { SchemaViolationError, MaxRetriesExceededError } from "@aviasole/shapecraft";
try {
const result = await generate(model, schema, prompt, { maxRetries: 3 });
} catch (err) {
if (err instanceof MaxRetriesExceededError) {
console.error(`Failed after ${err.attempts} attempts`);
}
if (err instanceof SchemaViolationError) {
console.error("Raw output:", err.raw);
console.error("Errors:", err.validationErrors);
}
}See the Error Handling guide and Options reference for the full list of generate() options.
Apache-2.0 © Aviasole Technologies

