Back to Articles

Rigging: The Anti-Framework Framework for Production LLM Code

[ View on GitHub ]

Rigging: The Anti-Framework Framework for Production LLM Code

Hook

What if working with LLMs felt less like orchestrating a complex system and more like calling a typed Python function? That’s the bet Rigging is making—and it might be the most productive framework you’ve never heard of.

Context

The LLM framework landscape is exhausting. LangChain offers everything including the kitchen sink, but you’ll spend hours navigating abstraction layers just to make a simple API call. LlamaIndex is brilliant for RAG, but overkill if you just need structured outputs from a chat model.

Rigging emerged from dreadnode’s internal needs: a team shipping production LLM applications daily wanted something that felt like native Python, not a framework. They needed type safety, async performance, and the ability to swap models without rewriting code. Most importantly, they wanted their prompts to look like code, not YAML configs or nested builder patterns. Rigging is the result—a library that treats LLM interactions as first-class Python functions with type hints, docstrings, and Pydantic validation baked in.

Technical Insight

The core insight behind Rigging is that prompts are just functions with unusual I/O. Instead of building elaborate chain objects, you write a Python function, decorate it, and let the framework handle the rest. Here’s the canonical example:

import rigging as rg

@rg.prompt(generator_id="gpt-4")
async def get_authors(count: int = 3) -> list[str]:
    """Provide famous authors."""

print(await get_authors())
# ['William Shakespeare', 'J.K. Rowling', 'Jane Austen']

This is deceptively simple. Under the hood, Rigging is parsing your function signature, extracting the docstring as the user prompt, converting the count parameter into a message, and using the return type annotation (list[str]) to configure Pydantic structured output parsing. The LLM doesn’t just generate text—it’s forced to return valid JSON matching your type hint, then validated and deserialized automatically.

The decorator syntax eliminates the impedance mismatch between your code and your prompts. Traditional frameworks make you build objects, configure parsers, and handle deserialization manually. Rigging collapses all of that into Python’s native syntax. Your IDE gets full autocomplete, type checkers can analyze your LLM calls, and refactoring tools work out of the box.

But the real power emerges when you drop down to the pipeline API for more control. Rigging’s architecture is built around three core abstractions: generators (model connections), pipelines (chat or completion sequences), and serializable conversations. Here’s a more complex example:

import rigging as rg
import asyncio

async def main():
    generator = rg.get_generator("claude-3-sonnet-20240229")
    
    pipeline = generator.chat(
        [
            {"role": "system", "content": "Talk like a pirate."},
            {"role": "user", "content": "Say hello!"},
        ]
    )
    
    chat = await pipeline.run()
    print(chat.conversation)

asyncio.run(main())

The rg.get_generator() function uses connection strings—the same pattern as database URIs. This means you can store model configurations as environment variables or config files: gpt-4-turbo,temperature=0.7,api_key=... works identically to claude-3-opus,temperature=0.7,api_key=.... Swap providers by changing a string, not refactoring code.

Rigging’s killer feature is universal tool support. Many models don’t natively support function calling at the API level, but Rigging implements it in the prompt layer. You define tools as Python functions, and the framework handles the serialization, injection, and parsing—even for models that have never seen a function call format:

from rigging import tool

@tool
def get_weather(location: str) -> str:
    """Get the current weather for a location."""
    return f"The weather in {location} is sunny."

pipeline = generator.chat(
    [{"role": "user", "content": "What's the weather in Tokyo?"}]
).with_tools([get_weather])

chat = await pipeline.run()
# The model can now call get_weather even if it doesn't support native tools

For production deployments, Rigging includes async batching for processing thousands of prompts efficiently, built-in tracing with Logfire for debugging, and serialization to save entire conversation trees to disk. The framework leverages LiteLLM as the default backend, giving you instant access to OpenAI, Anthropic, Mistral, Cohere, Hugging Face, and dozens more providers without writing adapter code. If you need more control, you can swap in vLLM or transformers for local model hosting.

The Pydantic integration deserves special attention. You can define complex structured outputs and Rigging will coerce the LLM to match your schema:

from pydantic import BaseModel

class Author(BaseModel):
    name: str
    birth_year: int
    notable_work: str

@rg.prompt(generator_id="gpt-4")
async def get_author_info(name: str) -> Author:
    """Provide detailed information about an author."""

result = await get_author_info("Mary Shelley")
print(result.birth_year)  # 1797, fully typed and validated

This isn’t just parsing JSON from the output—Rigging appears to use structured output capabilities intelligently based on what each model supports, ensuring you get validated Pydantic objects regardless of the underlying provider. You write the schema once, and the framework handles the rest.

Gotcha

Rigging’s minimalism is both its strength and its limitation. With 407 stars, you’re joining a smaller community than LangChain’s ecosystem of thousands of contributors. The documentation is solid, but you won’t find the endless tutorials, Stack Overflow answers, and third-party plugins that mature frameworks enjoy. If you hit an edge case, you might be the first person to report it.

The heavy reliance on LiteLLM means you inherit its quirks and limitations. If LiteLLM has a bug in its provider adapters or doesn’t support a bleeding-edge model parameter, you may be blocked until they address it. The framework is also explicitly built for dreadnode’s internal use cases, which means certain patterns may require more manual orchestration than in specialized frameworks. While the examples folder includes sophisticated agents (like the OverTheWire Bandit solver and Damn Vulnerable Restaurant Agent) and a RAG pipeline example, you’re expected to build orchestration logic yourself rather than relying on pre-built chains.

Finally, if you need synchronous code, you’re out of luck. Rigging is async-first, which is great for performance but adds complexity if you’re integrating with legacy codebases or running in environments where async is awkward.

Verdict

Use Rigging if you’re building production LLM applications where type safety, code cleanliness, and performance matter more than ecosystem size. It shines when you need structured outputs with Pydantic validation, want to swap models without refactoring, or are tired of fighting abstraction layers in heavier frameworks. It’s ideal for teams that value Pythonic APIs and already think in async patterns. The decorator syntax alone is worth it if you’re writing dozens of LLM-powered functions.

Skip it if you need battle-tested pre-built chains, extensive plugin ecosystems, or enterprise support with SLAs—LangChain and LlamaIndex are better choices there. Also skip if you’re uncomfortable with async Python. And if you’re prototyping quickly and don’t care about type safety or production concerns, something even simpler like direct API calls with the OpenAI SDK might be faster to start. But if you’re shipping LLM features daily and craving a framework that feels like it respects your time, Rigging deserves serious consideration.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/ai-agents/dreadnode-rigging.svg)](https://starlog.is/api/badge-click/ai-agents/dreadnode-rigging)