Engine-agnostic structured-output abstraction for LLMs — Task trait, Grammar enum (JSON Schema, Lark, Regex), and the canonical ImageAnalysis data type. Decouples a prompt + grammar + parser from any specific inference backend.
Overview
llmtask is the engine-agnostic side of every "structured output from an LLM" pipeline:
Task— a trait carrying the four things every constrained-decoding call needs: a prompt, a borrowed schema (type Value), a grammar wrapper (Grammarenum), and a typed parser (type Output,type ParseError). Engines accept anyT: Task<Value = ...>, so aTaskwritten once runs against any engine in the ecosystem (lfm,qwen, …) without translation.Grammar— an enum over the constrained-decoding surfaces real engines accept: JSON Schema (Grammar::JsonSchema, behind thejsonfeature), Lark (Grammar::Lark), and Regex (Grammar::Regex(RegexGrammar), behind theregexfeature — the wrapper holds both the source pattern and a default-options compiled regex, guaranteeing engine grammar and local validation describe the same language). Engines pattern-match and returnUnsupportedGrammarwhen they don't speak a given variant — the caller can then route to a different backend.ImageAnalysis— the canonical single-image VLM output shape (scene category, description, subjects/objects/actions/mood/lighting lists, shot-type label, search tags). Lets multiple VLM engines (lfm,qwen) produce values of the same type so downstream consumers compare and merge results without per-engine adapters.
Why an engine-agnostic Task layer?
Without a shared trait, every "structured output" prompt re-implements the same plumbing per engine: prompt string, schema construction, parser, error type, validation predicate. The plumbing is engine-independent — the same ImageAnalysis JSON Schema works against lfm's llguidance backend and qwen's mistralrs backend; only the constraint API differs.
llmtask separates them:
┌──────────────────────────┐
YourTask: impl Task ──▶ │ llmtask::Task contract │ ──▶ any engine that
│ prompt + Grammar │ takes &impl Task
│ parse → Output │
└──────────────────────────┘
A Task written today against a JSON Schema runs through lfm (llguidance) and qwen (mistralrs) unchanged. A Task returning a Lark grammar runs through lfm natively; qwen rejects it cleanly via Error::UnsupportedGrammar so the caller can dispatch elsewhere.
Features
- Engine-agnostic
Tasktrait with associatedOutput,Value, andParseErrortypes — engines bound to a specific schema kind get typed access (fn run<T: Task<Value = serde_json::Value>>). - Three grammar surfaces in a single
#[non_exhaustive]enum: JSON Schema (default), Lark, and pre-compiledregex::Regex. Engines pattern-match and route. UnsupportedGrammarerror carrying the rejected variantkind()and the engine'ssupportedlist — callers can route to a different engine when one variant isn't accepted.- Optional
jsonfeature (default-on) —Grammar::JsonSchema(serde_json::Value)plus theJsonParseErrorconvenience type. Drop it viadefault-features = false, features = ["alloc"](or"regex"/"serde", both of which implyalloc) to get a Lark-or-Regex-only build with noserde_jsondep. NOTE:allocis required to reach any public API —default-features = falsealone exposes nothing. - Optional
regexfeature — pre-compiledregex::Regexin the variant (validation enforced by the type), plusas_regex()/as_regex_pattern()helpers. - Optional
serdefeature —Serialize/DeserializeonImageAnalysisfor downstream wire formats. - Canonical
ImageAnalysis— nine-field single-image VLM output shape with builder-style API (with_*/set_*), shared across the findit-studio engines.
Example
// Marked `ignore` so doctests don't pull serde_derive under
// default features (`std + json` only). The example needs
// `--features serde` to compile (the `serde::Deserialize`
// derive); enable that on the dev-deps in any consumer who
// wants to lift this verbatim.
use OnceLock;
use ;
use ;
/// A minimal Task: "summarize what's in this image" as a JSON object
/// with a single `summary` string field.
;
// `&SummaryTask` now satisfies any engine taking `&impl Task<Value = Value>`:
// lfm::Engine::run(&SummaryTask, &images, &opts)
// qwen::Engine::run(&SummaryTask, images).await
// And the canonical multi-field VLM output type lives right here:
let _empty = new;
A regex-only Task (no JSON dep at all):
// Marked `ignore` so doctests don't try to compile this under
// default features (the `regex` feature is opt-in).
use ;
use SmolStr;
;
Installation
[]
# Default: JSON Schema support on, Lark always available, regex off, serde off.
= "0.1"
# Lark-only build (no serde_json, no regex):
# `alloc` is required — without it the public API is empty.
= { = "0.1", = false, = ["alloc"] }
# Regex-only build (no serde_json; `regex` already implies `alloc`):
= { = "0.1", = false, = ["regex"] }
# Everything:
= { = "0.1", = ["json", "regex", "serde"] }
| Feature | Default | What it adds |
|---|---|---|
json |
yes | Grammar::JsonSchema(serde_json::Value) variant + JsonParseError + the serde_json dep |
regex |
no | Grammar::Regex(RegexGrammar) variant + validating Grammar::regex constructor + regex dep |
serde |
no | Serialize / Deserialize on ImageAnalysis |
MSRV
Rust 1.95.
License
llmtask is under the terms of both the MIT license and the
Apache License (Version 2.0).
See LICENSE-APACHE, LICENSE-MIT for details.
Copyright (c) 2026 FinDIT Studio authors.